Merge branch 'HKUDS:main' into main
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
from .lightrag import LightRAG as LightRAG, QueryParam as QueryParam
|
from .lightrag import LightRAG as LightRAG, QueryParam as QueryParam
|
||||||
|
|
||||||
__version__ = "1.2.4"
|
__version__ = "1.2.5"
|
||||||
__author__ = "Zirui Guo"
|
__author__ = "Zirui Guo"
|
||||||
__url__ = "https://github.com/HKUDS/LightRAG"
|
__url__ = "https://github.com/HKUDS/LightRAG"
|
||||||
|
@@ -50,9 +50,6 @@ from .auth import auth_handler
|
|||||||
# This update allows the user to put a different.env file for each lightrag folder
|
# This update allows the user to put a different.env file for each lightrag folder
|
||||||
load_dotenv(".env", override=True)
|
load_dotenv(".env", override=True)
|
||||||
|
|
||||||
# Read entity extraction cache config
|
|
||||||
enable_llm_cache = os.getenv("ENABLE_LLM_CACHE_FOR_EXTRACT", "false").lower() == "true"
|
|
||||||
|
|
||||||
# Initialize config parser
|
# Initialize config parser
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
config.read("config.ini")
|
config.read("config.ini")
|
||||||
@@ -144,23 +141,25 @@ def create_app(args):
|
|||||||
try:
|
try:
|
||||||
# Initialize database connections
|
# Initialize database connections
|
||||||
await rag.initialize_storages()
|
await rag.initialize_storages()
|
||||||
await initialize_pipeline_status()
|
|
||||||
|
|
||||||
|
await initialize_pipeline_status()
|
||||||
|
pipeline_status = await get_namespace_data("pipeline_status")
|
||||||
|
|
||||||
|
should_start_autoscan = False
|
||||||
|
async with get_pipeline_status_lock():
|
||||||
# Auto scan documents if enabled
|
# Auto scan documents if enabled
|
||||||
if args.auto_scan_at_startup:
|
if args.auto_scan_at_startup:
|
||||||
# Check if a task is already running (with lock protection)
|
if not pipeline_status.get("autoscanned", False):
|
||||||
pipeline_status = await get_namespace_data("pipeline_status")
|
pipeline_status["autoscanned"] = True
|
||||||
should_start_task = False
|
should_start_autoscan = True
|
||||||
async with get_pipeline_status_lock():
|
|
||||||
if not pipeline_status.get("busy", False):
|
# Only run auto scan when no other process started it first
|
||||||
should_start_task = True
|
if should_start_autoscan:
|
||||||
# Only start the task if no other task is running
|
|
||||||
if should_start_task:
|
|
||||||
# Create background task
|
# Create background task
|
||||||
task = asyncio.create_task(run_scanning_process(rag, doc_manager))
|
task = asyncio.create_task(run_scanning_process(rag, doc_manager))
|
||||||
app.state.background_tasks.add(task)
|
app.state.background_tasks.add(task)
|
||||||
task.add_done_callback(app.state.background_tasks.discard)
|
task.add_done_callback(app.state.background_tasks.discard)
|
||||||
logger.info("Auto scan task started at startup.")
|
logger.info(f"Process {os.getpid()} auto scan task started at startup.")
|
||||||
|
|
||||||
ASCIIColors.green("\nServer is ready to accept connections! 🚀\n")
|
ASCIIColors.green("\nServer is ready to accept connections! 🚀\n")
|
||||||
|
|
||||||
@@ -326,7 +325,7 @@ def create_app(args):
|
|||||||
vector_db_storage_cls_kwargs={
|
vector_db_storage_cls_kwargs={
|
||||||
"cosine_better_than_threshold": args.cosine_threshold
|
"cosine_better_than_threshold": args.cosine_threshold
|
||||||
},
|
},
|
||||||
enable_llm_cache_for_entity_extract=enable_llm_cache, # Read from environment variable
|
enable_llm_cache_for_entity_extract=args.enable_llm_cache_for_extract,
|
||||||
embedding_cache_config={
|
embedding_cache_config={
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"similarity_threshold": 0.95,
|
"similarity_threshold": 0.95,
|
||||||
@@ -355,7 +354,7 @@ def create_app(args):
|
|||||||
vector_db_storage_cls_kwargs={
|
vector_db_storage_cls_kwargs={
|
||||||
"cosine_better_than_threshold": args.cosine_threshold
|
"cosine_better_than_threshold": args.cosine_threshold
|
||||||
},
|
},
|
||||||
enable_llm_cache_for_entity_extract=enable_llm_cache, # Read from environment variable
|
enable_llm_cache_for_entity_extract=args.enable_llm_cache_for_extract,
|
||||||
embedding_cache_config={
|
embedding_cache_config={
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"similarity_threshold": 0.95,
|
"similarity_threshold": 0.95,
|
||||||
@@ -419,6 +418,7 @@ def create_app(args):
|
|||||||
"doc_status_storage": args.doc_status_storage,
|
"doc_status_storage": args.doc_status_storage,
|
||||||
"graph_storage": args.graph_storage,
|
"graph_storage": args.graph_storage,
|
||||||
"vector_storage": args.vector_storage,
|
"vector_storage": args.vector_storage,
|
||||||
|
"enable_llm_cache_for_extract": args.enable_llm_cache_for_extract,
|
||||||
},
|
},
|
||||||
"update_status": update_status,
|
"update_status": update_status,
|
||||||
}
|
}
|
||||||
|
@@ -16,7 +16,11 @@ from pydantic import BaseModel, Field, field_validator
|
|||||||
|
|
||||||
from lightrag import LightRAG
|
from lightrag import LightRAG
|
||||||
from lightrag.base import DocProcessingStatus, DocStatus
|
from lightrag.base import DocProcessingStatus, DocStatus
|
||||||
from ..utils_api import get_api_key_dependency, get_auth_dependency
|
from lightrag.api.utils_api import (
|
||||||
|
get_api_key_dependency,
|
||||||
|
global_args,
|
||||||
|
get_auth_dependency,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
prefix="/documents",
|
prefix="/documents",
|
||||||
@@ -240,6 +244,15 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
case ".pdf":
|
case ".pdf":
|
||||||
|
if global_args["main_args"].document_loading_engine == "DOCLING":
|
||||||
|
if not pm.is_installed("docling"): # type: ignore
|
||||||
|
pm.install("docling")
|
||||||
|
from docling.document_converter import DocumentConverter
|
||||||
|
|
||||||
|
converter = DocumentConverter()
|
||||||
|
result = converter.convert(file_path)
|
||||||
|
content = result.document.export_to_markdown()
|
||||||
|
else:
|
||||||
if not pm.is_installed("pypdf2"): # type: ignore
|
if not pm.is_installed("pypdf2"): # type: ignore
|
||||||
pm.install("pypdf2")
|
pm.install("pypdf2")
|
||||||
from PyPDF2 import PdfReader # type: ignore
|
from PyPDF2 import PdfReader # type: ignore
|
||||||
@@ -250,6 +263,15 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
|
|||||||
for page in reader.pages:
|
for page in reader.pages:
|
||||||
content += page.extract_text() + "\n"
|
content += page.extract_text() + "\n"
|
||||||
case ".docx":
|
case ".docx":
|
||||||
|
if global_args["main_args"].document_loading_engine == "DOCLING":
|
||||||
|
if not pm.is_installed("docling"): # type: ignore
|
||||||
|
pm.install("docling")
|
||||||
|
from docling.document_converter import DocumentConverter
|
||||||
|
|
||||||
|
converter = DocumentConverter()
|
||||||
|
result = converter.convert(file_path)
|
||||||
|
content = result.document.export_to_markdown()
|
||||||
|
else:
|
||||||
if not pm.is_installed("python-docx"): # type: ignore
|
if not pm.is_installed("python-docx"): # type: ignore
|
||||||
pm.install("docx")
|
pm.install("docx")
|
||||||
from docx import Document # type: ignore
|
from docx import Document # type: ignore
|
||||||
@@ -257,8 +279,19 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
|
|||||||
|
|
||||||
docx_file = BytesIO(file)
|
docx_file = BytesIO(file)
|
||||||
doc = Document(docx_file)
|
doc = Document(docx_file)
|
||||||
content = "\n".join([paragraph.text for paragraph in doc.paragraphs])
|
content = "\n".join(
|
||||||
|
[paragraph.text for paragraph in doc.paragraphs]
|
||||||
|
)
|
||||||
case ".pptx":
|
case ".pptx":
|
||||||
|
if global_args["main_args"].document_loading_engine == "DOCLING":
|
||||||
|
if not pm.is_installed("docling"): # type: ignore
|
||||||
|
pm.install("docling")
|
||||||
|
from docling.document_converter import DocumentConverter
|
||||||
|
|
||||||
|
converter = DocumentConverter()
|
||||||
|
result = converter.convert(file_path)
|
||||||
|
content = result.document.export_to_markdown()
|
||||||
|
else:
|
||||||
if not pm.is_installed("python-pptx"): # type: ignore
|
if not pm.is_installed("python-pptx"): # type: ignore
|
||||||
pm.install("pptx")
|
pm.install("pptx")
|
||||||
from pptx import Presentation # type: ignore
|
from pptx import Presentation # type: ignore
|
||||||
@@ -271,6 +304,15 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
|
|||||||
if hasattr(shape, "text"):
|
if hasattr(shape, "text"):
|
||||||
content += shape.text + "\n"
|
content += shape.text + "\n"
|
||||||
case ".xlsx":
|
case ".xlsx":
|
||||||
|
if global_args["main_args"].document_loading_engine == "DOCLING":
|
||||||
|
if not pm.is_installed("docling"): # type: ignore
|
||||||
|
pm.install("docling")
|
||||||
|
from docling.document_converter import DocumentConverter
|
||||||
|
|
||||||
|
converter = DocumentConverter()
|
||||||
|
result = converter.convert(file_path)
|
||||||
|
content = result.document.export_to_markdown()
|
||||||
|
else:
|
||||||
if not pm.is_installed("openpyxl"): # type: ignore
|
if not pm.is_installed("openpyxl"): # type: ignore
|
||||||
pm.install("openpyxl")
|
pm.install("openpyxl")
|
||||||
from openpyxl import load_workbook # type: ignore
|
from openpyxl import load_workbook # type: ignore
|
||||||
@@ -283,7 +325,8 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
|
|||||||
for row in sheet.iter_rows(values_only=True):
|
for row in sheet.iter_rows(values_only=True):
|
||||||
content += (
|
content += (
|
||||||
"\t".join(
|
"\t".join(
|
||||||
str(cell) if cell is not None else "" for cell in row
|
str(cell) if cell is not None else ""
|
||||||
|
for cell in row
|
||||||
)
|
)
|
||||||
+ "\n"
|
+ "\n"
|
||||||
)
|
)
|
||||||
|
@@ -11,7 +11,7 @@ import asyncio
|
|||||||
from ascii_colors import trace_exception
|
from ascii_colors import trace_exception
|
||||||
from lightrag import LightRAG, QueryParam
|
from lightrag import LightRAG, QueryParam
|
||||||
from lightrag.utils import encode_string_by_tiktoken
|
from lightrag.utils import encode_string_by_tiktoken
|
||||||
from ..utils_api import ollama_server_infos
|
from lightrag.api.utils_api import ollama_server_infos
|
||||||
|
|
||||||
|
|
||||||
# query mode according to query prefix (bypass is not LightRAG quer mode)
|
# query mode according to query prefix (bypass is not LightRAG quer mode)
|
||||||
|
@@ -18,6 +18,8 @@ from .auth import auth_handler
|
|||||||
# Load environment variables
|
# Load environment variables
|
||||||
load_dotenv(override=True)
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
global_args = {"main_args": None}
|
||||||
|
|
||||||
|
|
||||||
class OllamaServerInfos:
|
class OllamaServerInfos:
|
||||||
# Constants for emulated Ollama model information
|
# Constants for emulated Ollama model information
|
||||||
@@ -360,8 +362,17 @@ def parse_args(is_uvicorn_mode: bool = False) -> argparse.Namespace:
|
|||||||
args.chunk_size = get_env_value("CHUNK_SIZE", 1200, int)
|
args.chunk_size = get_env_value("CHUNK_SIZE", 1200, int)
|
||||||
args.chunk_overlap_size = get_env_value("CHUNK_OVERLAP_SIZE", 100, int)
|
args.chunk_overlap_size = get_env_value("CHUNK_OVERLAP_SIZE", 100, int)
|
||||||
|
|
||||||
|
# Inject LLM cache configuration
|
||||||
|
args.enable_llm_cache_for_extract = get_env_value(
|
||||||
|
"ENABLE_LLM_CACHE_FOR_EXTRACT", False, bool
|
||||||
|
)
|
||||||
|
|
||||||
|
# Select Document loading tool (DOCLING, DEFAULT)
|
||||||
|
args.document_loading_engine = get_env_value("DOCUMENT_LOADING_ENGINE", "DEFAULT")
|
||||||
|
|
||||||
ollama_server_infos.LIGHTRAG_MODEL = args.simulated_model_name
|
ollama_server_infos.LIGHTRAG_MODEL = args.simulated_model_name
|
||||||
|
|
||||||
|
global_args["main_args"] = args
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
@@ -451,8 +462,10 @@ def display_splash_screen(args: argparse.Namespace) -> None:
|
|||||||
ASCIIColors.yellow(f"{args.history_turns}")
|
ASCIIColors.yellow(f"{args.history_turns}")
|
||||||
ASCIIColors.white(" ├─ Cosine Threshold: ", end="")
|
ASCIIColors.white(" ├─ Cosine Threshold: ", end="")
|
||||||
ASCIIColors.yellow(f"{args.cosine_threshold}")
|
ASCIIColors.yellow(f"{args.cosine_threshold}")
|
||||||
ASCIIColors.white(" └─ Top-K: ", end="")
|
ASCIIColors.white(" ├─ Top-K: ", end="")
|
||||||
ASCIIColors.yellow(f"{args.top_k}")
|
ASCIIColors.yellow(f"{args.top_k}")
|
||||||
|
ASCIIColors.white(" └─ LLM Cache for Extraction Enabled: ", end="")
|
||||||
|
ASCIIColors.yellow(f"{args.enable_llm_cache_for_extract}")
|
||||||
|
|
||||||
# System Configuration
|
# System Configuration
|
||||||
ASCIIColors.magenta("\n💾 Storage Configuration:")
|
ASCIIColors.magenta("\n💾 Storage Configuration:")
|
||||||
|
@@ -127,6 +127,30 @@ class BaseVectorStorage(StorageNameSpace, ABC):
|
|||||||
async def delete_entity_relation(self, entity_name: str) -> None:
|
async def delete_entity_relation(self, entity_name: str) -> None:
|
||||||
"""Delete relations for a given entity."""
|
"""Delete relations for a given entity."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_by_id(self, id: str) -> dict[str, Any] | None:
|
||||||
|
"""Get vector data by its ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id: The unique identifier of the vector
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The vector data if found, or None if not found
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
|
||||||
|
"""Get multiple vector data by their IDs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ids: List of unique identifiers
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of vector data objects that were found
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BaseKVStorage(StorageNameSpace, ABC):
|
class BaseKVStorage(StorageNameSpace, ABC):
|
||||||
|
@@ -271,3 +271,67 @@ class ChromaVectorDBStorage(BaseVectorStorage):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error during prefix search in ChromaDB: {str(e)}")
|
logger.error(f"Error during prefix search in ChromaDB: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
async def get_by_id(self, id: str) -> dict[str, Any] | None:
|
||||||
|
"""Get vector data by its ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id: The unique identifier of the vector
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The vector data if found, or None if not found
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Query the collection for a single vector by ID
|
||||||
|
result = self._collection.get(
|
||||||
|
ids=[id], include=["metadatas", "embeddings", "documents"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result or not result["ids"] or len(result["ids"]) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Format the result to match the expected structure
|
||||||
|
return {
|
||||||
|
"id": result["ids"][0],
|
||||||
|
"vector": result["embeddings"][0],
|
||||||
|
"content": result["documents"][0],
|
||||||
|
**result["metadatas"][0],
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving vector data for ID {id}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
|
||||||
|
"""Get multiple vector data by their IDs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ids: List of unique identifiers
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of vector data objects that were found
|
||||||
|
"""
|
||||||
|
if not ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Query the collection for multiple vectors by IDs
|
||||||
|
result = self._collection.get(
|
||||||
|
ids=ids, include=["metadatas", "embeddings", "documents"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result or not result["ids"] or len(result["ids"]) == 0:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Format the results to match the expected structure
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": result["ids"][i],
|
||||||
|
"vector": result["embeddings"][i],
|
||||||
|
"content": result["documents"][i],
|
||||||
|
**result["metadatas"][i],
|
||||||
|
}
|
||||||
|
for i in range(len(result["ids"]))
|
||||||
|
]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
|
||||||
|
return []
|
||||||
|
@@ -394,3 +394,46 @@ class FaissVectorDBStorage(BaseVectorStorage):
|
|||||||
|
|
||||||
logger.debug(f"Found {len(matching_records)} records with prefix '{prefix}'")
|
logger.debug(f"Found {len(matching_records)} records with prefix '{prefix}'")
|
||||||
return matching_records
|
return matching_records
|
||||||
|
|
||||||
|
async def get_by_id(self, id: str) -> dict[str, Any] | None:
|
||||||
|
"""Get vector data by its ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id: The unique identifier of the vector
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The vector data if found, or None if not found
|
||||||
|
"""
|
||||||
|
# Find the Faiss internal ID for the custom ID
|
||||||
|
fid = self._find_faiss_id_by_custom_id(id)
|
||||||
|
if fid is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get the metadata for the found ID
|
||||||
|
metadata = self._id_to_meta.get(fid, {})
|
||||||
|
if not metadata:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {**metadata, "id": metadata.get("__id__")}
|
||||||
|
|
||||||
|
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
|
||||||
|
"""Get multiple vector data by their IDs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ids: List of unique identifiers
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of vector data objects that were found
|
||||||
|
"""
|
||||||
|
if not ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for id in ids:
|
||||||
|
fid = self._find_faiss_id_by_custom_id(id)
|
||||||
|
if fid is not None:
|
||||||
|
metadata = self._id_to_meta.get(fid, {})
|
||||||
|
if metadata:
|
||||||
|
results.append({**metadata, "id": metadata.get("__id__")})
|
||||||
|
|
||||||
|
return results
|
||||||
|
@@ -15,6 +15,10 @@ from lightrag.utils import (
|
|||||||
from .shared_storage import (
|
from .shared_storage import (
|
||||||
get_namespace_data,
|
get_namespace_data,
|
||||||
get_storage_lock,
|
get_storage_lock,
|
||||||
|
get_data_init_lock,
|
||||||
|
get_update_flag,
|
||||||
|
set_all_update_flags,
|
||||||
|
clear_all_update_flags,
|
||||||
try_initialize_namespace,
|
try_initialize_namespace,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,20 +31,24 @@ class JsonDocStatusStorage(DocStatusStorage):
|
|||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
working_dir = self.global_config["working_dir"]
|
working_dir = self.global_config["working_dir"]
|
||||||
self._file_name = os.path.join(working_dir, f"kv_store_{self.namespace}.json")
|
self._file_name = os.path.join(working_dir, f"kv_store_{self.namespace}.json")
|
||||||
self._storage_lock = get_storage_lock()
|
|
||||||
self._data = None
|
self._data = None
|
||||||
|
self._storage_lock = None
|
||||||
|
self.storage_updated = None
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
"""Initialize storage data"""
|
"""Initialize storage data"""
|
||||||
|
self._storage_lock = get_storage_lock()
|
||||||
|
self.storage_updated = await get_update_flag(self.namespace)
|
||||||
|
async with get_data_init_lock():
|
||||||
# check need_init must before get_namespace_data
|
# check need_init must before get_namespace_data
|
||||||
need_init = try_initialize_namespace(self.namespace)
|
need_init = await try_initialize_namespace(self.namespace)
|
||||||
self._data = await get_namespace_data(self.namespace)
|
self._data = await get_namespace_data(self.namespace)
|
||||||
if need_init:
|
if need_init:
|
||||||
loaded_data = load_json(self._file_name) or {}
|
loaded_data = load_json(self._file_name) or {}
|
||||||
async with self._storage_lock:
|
async with self._storage_lock:
|
||||||
self._data.update(loaded_data)
|
self._data.update(loaded_data)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Loaded document status storage with {len(loaded_data)} records"
|
f"Process {os.getpid()} doc status load {self.namespace} with {len(loaded_data)} records"
|
||||||
)
|
)
|
||||||
|
|
||||||
async def filter_keys(self, keys: set[str]) -> set[str]:
|
async def filter_keys(self, keys: set[str]) -> set[str]:
|
||||||
@@ -87,18 +95,24 @@ class JsonDocStatusStorage(DocStatusStorage):
|
|||||||
|
|
||||||
async def index_done_callback(self) -> None:
|
async def index_done_callback(self) -> None:
|
||||||
async with self._storage_lock:
|
async with self._storage_lock:
|
||||||
|
if self.storage_updated.value:
|
||||||
data_dict = (
|
data_dict = (
|
||||||
dict(self._data) if hasattr(self._data, "_getvalue") else self._data
|
dict(self._data) if hasattr(self._data, "_getvalue") else self._data
|
||||||
)
|
)
|
||||||
|
logger.info(
|
||||||
|
f"Process {os.getpid()} doc status writting {len(data_dict)} records to {self.namespace}"
|
||||||
|
)
|
||||||
write_json(data_dict, self._file_name)
|
write_json(data_dict, self._file_name)
|
||||||
|
await clear_all_update_flags(self.namespace)
|
||||||
|
|
||||||
async def upsert(self, data: dict[str, dict[str, Any]]) -> None:
|
async def upsert(self, data: dict[str, dict[str, Any]]) -> None:
|
||||||
logger.info(f"Inserting {len(data)} to {self.namespace}")
|
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
|
logger.info(f"Inserting {len(data)} records to {self.namespace}")
|
||||||
async with self._storage_lock:
|
async with self._storage_lock:
|
||||||
self._data.update(data)
|
self._data.update(data)
|
||||||
|
await set_all_update_flags(self.namespace)
|
||||||
|
|
||||||
await self.index_done_callback()
|
await self.index_done_callback()
|
||||||
|
|
||||||
async def get_by_id(self, id: str) -> Union[dict[str, Any], None]:
|
async def get_by_id(self, id: str) -> Union[dict[str, Any], None]:
|
||||||
@@ -109,9 +123,12 @@ class JsonDocStatusStorage(DocStatusStorage):
|
|||||||
async with self._storage_lock:
|
async with self._storage_lock:
|
||||||
for doc_id in doc_ids:
|
for doc_id in doc_ids:
|
||||||
self._data.pop(doc_id, None)
|
self._data.pop(doc_id, None)
|
||||||
|
await set_all_update_flags(self.namespace)
|
||||||
await self.index_done_callback()
|
await self.index_done_callback()
|
||||||
|
|
||||||
async def drop(self) -> None:
|
async def drop(self) -> None:
|
||||||
"""Drop the storage"""
|
"""Drop the storage"""
|
||||||
async with self._storage_lock:
|
async with self._storage_lock:
|
||||||
self._data.clear()
|
self._data.clear()
|
||||||
|
await set_all_update_flags(self.namespace)
|
||||||
|
await self.index_done_callback()
|
||||||
|
@@ -13,6 +13,10 @@ from lightrag.utils import (
|
|||||||
from .shared_storage import (
|
from .shared_storage import (
|
||||||
get_namespace_data,
|
get_namespace_data,
|
||||||
get_storage_lock,
|
get_storage_lock,
|
||||||
|
get_data_init_lock,
|
||||||
|
get_update_flag,
|
||||||
|
set_all_update_flags,
|
||||||
|
clear_all_update_flags,
|
||||||
try_initialize_namespace,
|
try_initialize_namespace,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,26 +27,63 @@ class JsonKVStorage(BaseKVStorage):
|
|||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
working_dir = self.global_config["working_dir"]
|
working_dir = self.global_config["working_dir"]
|
||||||
self._file_name = os.path.join(working_dir, f"kv_store_{self.namespace}.json")
|
self._file_name = os.path.join(working_dir, f"kv_store_{self.namespace}.json")
|
||||||
self._storage_lock = get_storage_lock()
|
|
||||||
self._data = None
|
self._data = None
|
||||||
|
self._storage_lock = None
|
||||||
|
self.storage_updated = None
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
"""Initialize storage data"""
|
"""Initialize storage data"""
|
||||||
|
self._storage_lock = get_storage_lock()
|
||||||
|
self.storage_updated = await get_update_flag(self.namespace)
|
||||||
|
async with get_data_init_lock():
|
||||||
# check need_init must before get_namespace_data
|
# check need_init must before get_namespace_data
|
||||||
need_init = try_initialize_namespace(self.namespace)
|
need_init = await try_initialize_namespace(self.namespace)
|
||||||
self._data = await get_namespace_data(self.namespace)
|
self._data = await get_namespace_data(self.namespace)
|
||||||
if need_init:
|
if need_init:
|
||||||
loaded_data = load_json(self._file_name) or {}
|
loaded_data = load_json(self._file_name) or {}
|
||||||
async with self._storage_lock:
|
async with self._storage_lock:
|
||||||
self._data.update(loaded_data)
|
self._data.update(loaded_data)
|
||||||
logger.info(f"Load KV {self.namespace} with {len(loaded_data)} data")
|
|
||||||
|
# Calculate data count based on namespace
|
||||||
|
if self.namespace.endswith("cache"):
|
||||||
|
# For cache namespaces, sum the cache entries across all cache types
|
||||||
|
data_count = sum(
|
||||||
|
len(first_level_dict)
|
||||||
|
for first_level_dict in loaded_data.values()
|
||||||
|
if isinstance(first_level_dict, dict)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# For non-cache namespaces, use the original count method
|
||||||
|
data_count = len(loaded_data)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Process {os.getpid()} KV load {self.namespace} with {data_count} records"
|
||||||
|
)
|
||||||
|
|
||||||
async def index_done_callback(self) -> None:
|
async def index_done_callback(self) -> None:
|
||||||
async with self._storage_lock:
|
async with self._storage_lock:
|
||||||
|
if self.storage_updated.value:
|
||||||
data_dict = (
|
data_dict = (
|
||||||
dict(self._data) if hasattr(self._data, "_getvalue") else self._data
|
dict(self._data) if hasattr(self._data, "_getvalue") else self._data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Calculate data count based on namespace
|
||||||
|
if self.namespace.endswith("cache"):
|
||||||
|
# # For cache namespaces, sum the cache entries across all cache types
|
||||||
|
data_count = sum(
|
||||||
|
len(first_level_dict)
|
||||||
|
for first_level_dict in data_dict.values()
|
||||||
|
if isinstance(first_level_dict, dict)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# For non-cache namespaces, use the original count method
|
||||||
|
data_count = len(data_dict)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Process {os.getpid()} KV writting {data_count} records to {self.namespace}"
|
||||||
|
)
|
||||||
write_json(data_dict, self._file_name)
|
write_json(data_dict, self._file_name)
|
||||||
|
await clear_all_update_flags(self.namespace)
|
||||||
|
|
||||||
async def get_all(self) -> dict[str, Any]:
|
async def get_all(self) -> dict[str, Any]:
|
||||||
"""Get all data from storage
|
"""Get all data from storage
|
||||||
@@ -73,15 +114,16 @@ class JsonKVStorage(BaseKVStorage):
|
|||||||
return set(keys) - set(self._data.keys())
|
return set(keys) - set(self._data.keys())
|
||||||
|
|
||||||
async def upsert(self, data: dict[str, dict[str, Any]]) -> None:
|
async def upsert(self, data: dict[str, dict[str, Any]]) -> None:
|
||||||
logger.info(f"Inserting {len(data)} to {self.namespace}")
|
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
|
logger.info(f"Inserting {len(data)} records to {self.namespace}")
|
||||||
async with self._storage_lock:
|
async with self._storage_lock:
|
||||||
left_data = {k: v for k, v in data.items() if k not in self._data}
|
self._data.update(data)
|
||||||
self._data.update(left_data)
|
await set_all_update_flags(self.namespace)
|
||||||
|
|
||||||
async def delete(self, ids: list[str]) -> None:
|
async def delete(self, ids: list[str]) -> None:
|
||||||
async with self._storage_lock:
|
async with self._storage_lock:
|
||||||
for doc_id in ids:
|
for doc_id in ids:
|
||||||
self._data.pop(doc_id, None)
|
self._data.pop(doc_id, None)
|
||||||
|
await set_all_update_flags(self.namespace)
|
||||||
await self.index_done_callback()
|
await self.index_done_callback()
|
||||||
|
@@ -233,3 +233,57 @@ class MilvusVectorDBStorage(BaseVectorStorage):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error searching for records with prefix '{prefix}': {e}")
|
logger.error(f"Error searching for records with prefix '{prefix}': {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
async def get_by_id(self, id: str) -> dict[str, Any] | None:
|
||||||
|
"""Get vector data by its ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id: The unique identifier of the vector
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The vector data if found, or None if not found
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Query Milvus for a specific ID
|
||||||
|
result = self._client.query(
|
||||||
|
collection_name=self.namespace,
|
||||||
|
filter=f'id == "{id}"',
|
||||||
|
output_fields=list(self.meta_fields) + ["id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result or len(result) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return result[0]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving vector data for ID {id}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
|
||||||
|
"""Get multiple vector data by their IDs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ids: List of unique identifiers
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of vector data objects that were found
|
||||||
|
"""
|
||||||
|
if not ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Prepare the ID filter expression
|
||||||
|
id_list = '", "'.join(ids)
|
||||||
|
filter_expr = f'id in ["{id_list}"]'
|
||||||
|
|
||||||
|
# Query Milvus with the filter
|
||||||
|
result = self._client.query(
|
||||||
|
collection_name=self.namespace,
|
||||||
|
filter=filter_expr,
|
||||||
|
output_fields=list(self.meta_fields) + ["id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return result or []
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
|
||||||
|
return []
|
||||||
|
@@ -1073,6 +1073,59 @@ class MongoVectorDBStorage(BaseVectorStorage):
|
|||||||
logger.error(f"Error searching by prefix in {self.namespace}: {str(e)}")
|
logger.error(f"Error searching by prefix in {self.namespace}: {str(e)}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
async def get_by_id(self, id: str) -> dict[str, Any] | None:
|
||||||
|
"""Get vector data by its ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id: The unique identifier of the vector
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The vector data if found, or None if not found
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Search for the specific ID in MongoDB
|
||||||
|
result = await self._data.find_one({"_id": id})
|
||||||
|
if result:
|
||||||
|
# Format the result to include id field expected by API
|
||||||
|
result_dict = dict(result)
|
||||||
|
if "_id" in result_dict and "id" not in result_dict:
|
||||||
|
result_dict["id"] = result_dict["_id"]
|
||||||
|
return result_dict
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving vector data for ID {id}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
|
||||||
|
"""Get multiple vector data by their IDs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ids: List of unique identifiers
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of vector data objects that were found
|
||||||
|
"""
|
||||||
|
if not ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Query MongoDB for multiple IDs
|
||||||
|
cursor = self._data.find({"_id": {"$in": ids}})
|
||||||
|
results = await cursor.to_list(length=None)
|
||||||
|
|
||||||
|
# Format results to include id field expected by API
|
||||||
|
formatted_results = []
|
||||||
|
for result in results:
|
||||||
|
result_dict = dict(result)
|
||||||
|
if "_id" in result_dict and "id" not in result_dict:
|
||||||
|
result_dict["id"] = result_dict["_id"]
|
||||||
|
formatted_results.append(result_dict)
|
||||||
|
|
||||||
|
return formatted_results
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
async def get_or_create_collection(db: AsyncIOMotorDatabase, collection_name: str):
|
async def get_or_create_collection(db: AsyncIOMotorDatabase, collection_name: str):
|
||||||
collection_names = await db.list_collection_names()
|
collection_names = await db.list_collection_names()
|
||||||
|
@@ -258,3 +258,33 @@ class NanoVectorDBStorage(BaseVectorStorage):
|
|||||||
|
|
||||||
logger.debug(f"Found {len(matching_records)} records with prefix '{prefix}'")
|
logger.debug(f"Found {len(matching_records)} records with prefix '{prefix}'")
|
||||||
return matching_records
|
return matching_records
|
||||||
|
|
||||||
|
async def get_by_id(self, id: str) -> dict[str, Any] | None:
|
||||||
|
"""Get vector data by its ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id: The unique identifier of the vector
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The vector data if found, or None if not found
|
||||||
|
"""
|
||||||
|
client = await self._get_client()
|
||||||
|
result = client.get([id])
|
||||||
|
if result:
|
||||||
|
return result[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
|
||||||
|
"""Get multiple vector data by their IDs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ids: List of unique identifiers
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of vector data objects that were found
|
||||||
|
"""
|
||||||
|
if not ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
client = await self._get_client()
|
||||||
|
return client.get(ids)
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -531,6 +531,80 @@ class OracleVectorDBStorage(BaseVectorStorage):
|
|||||||
logger.error(f"Error searching records with prefix '{prefix}': {e}")
|
logger.error(f"Error searching records with prefix '{prefix}': {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
async def get_by_id(self, id: str) -> dict[str, Any] | None:
|
||||||
|
"""Get vector data by its ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id: The unique identifier of the vector
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The vector data if found, or None if not found
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Determine the table name based on namespace
|
||||||
|
table_name = namespace_to_table_name(self.namespace)
|
||||||
|
if not table_name:
|
||||||
|
logger.error(f"Unknown namespace for ID lookup: {self.namespace}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Create the appropriate ID field name based on namespace
|
||||||
|
id_field = "entity_id" if "NODES" in table_name else "relation_id"
|
||||||
|
if "CHUNKS" in table_name:
|
||||||
|
id_field = "chunk_id"
|
||||||
|
|
||||||
|
# Prepare and execute the query
|
||||||
|
query = f"""
|
||||||
|
SELECT * FROM {table_name}
|
||||||
|
WHERE {id_field} = :id AND workspace = :workspace
|
||||||
|
"""
|
||||||
|
params = {"id": id, "workspace": self.db.workspace}
|
||||||
|
|
||||||
|
result = await self.db.query(query, params)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving vector data for ID {id}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
|
||||||
|
"""Get multiple vector data by their IDs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ids: List of unique identifiers
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of vector data objects that were found
|
||||||
|
"""
|
||||||
|
if not ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Determine the table name based on namespace
|
||||||
|
table_name = namespace_to_table_name(self.namespace)
|
||||||
|
if not table_name:
|
||||||
|
logger.error(f"Unknown namespace for IDs lookup: {self.namespace}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Create the appropriate ID field name based on namespace
|
||||||
|
id_field = "entity_id" if "NODES" in table_name else "relation_id"
|
||||||
|
if "CHUNKS" in table_name:
|
||||||
|
id_field = "chunk_id"
|
||||||
|
|
||||||
|
# Format the list of IDs for SQL IN clause
|
||||||
|
ids_list = ", ".join([f"'{id}'" for id in ids])
|
||||||
|
|
||||||
|
# Prepare and execute the query
|
||||||
|
query = f"""
|
||||||
|
SELECT * FROM {table_name}
|
||||||
|
WHERE {id_field} IN ({ids_list}) AND workspace = :workspace
|
||||||
|
"""
|
||||||
|
params = {"workspace": self.db.workspace}
|
||||||
|
|
||||||
|
results = await self.db.query(query, params, multirows=True)
|
||||||
|
return results or []
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@@ -621,6 +621,60 @@ class PGVectorStorage(BaseVectorStorage):
|
|||||||
logger.error(f"Error during prefix search for '{prefix}': {e}")
|
logger.error(f"Error during prefix search for '{prefix}': {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
async def get_by_id(self, id: str) -> dict[str, Any] | None:
|
||||||
|
"""Get vector data by its ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id: The unique identifier of the vector
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The vector data if found, or None if not found
|
||||||
|
"""
|
||||||
|
table_name = namespace_to_table_name(self.namespace)
|
||||||
|
if not table_name:
|
||||||
|
logger.error(f"Unknown namespace for ID lookup: {self.namespace}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
query = f"SELECT * FROM {table_name} WHERE workspace=$1 AND id=$2"
|
||||||
|
params = {"workspace": self.db.workspace, "id": id}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await self.db.query(query, params)
|
||||||
|
if result:
|
||||||
|
return dict(result)
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving vector data for ID {id}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
|
||||||
|
"""Get multiple vector data by their IDs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ids: List of unique identifiers
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of vector data objects that were found
|
||||||
|
"""
|
||||||
|
if not ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
table_name = namespace_to_table_name(self.namespace)
|
||||||
|
if not table_name:
|
||||||
|
logger.error(f"Unknown namespace for IDs lookup: {self.namespace}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
ids_str = ",".join([f"'{id}'" for id in ids])
|
||||||
|
query = f"SELECT * FROM {table_name} WHERE workspace=$1 AND id IN ({ids_str})"
|
||||||
|
params = {"workspace": self.db.workspace}
|
||||||
|
|
||||||
|
try:
|
||||||
|
results = await self.db.query(query, params, multirows=True)
|
||||||
|
return [dict(record) for record in results]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@@ -7,11 +7,17 @@ from typing import Any, Dict, Optional, Union, TypeVar, Generic
|
|||||||
|
|
||||||
|
|
||||||
# Define a direct print function for critical logs that must be visible in all processes
|
# Define a direct print function for critical logs that must be visible in all processes
|
||||||
def direct_log(message, level="INFO"):
|
def direct_log(message, level="INFO", enable_output: bool = True):
|
||||||
"""
|
"""
|
||||||
Log a message directly to stderr to ensure visibility in all processes,
|
Log a message directly to stderr to ensure visibility in all processes,
|
||||||
including the Gunicorn master process.
|
including the Gunicorn master process.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: The message to log
|
||||||
|
level: Log level (default: "INFO")
|
||||||
|
enable_output: Whether to actually output the log (default: True)
|
||||||
"""
|
"""
|
||||||
|
if enable_output:
|
||||||
print(f"{level}: {message}", file=sys.stderr, flush=True)
|
print(f"{level}: {message}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -32,55 +38,165 @@ _update_flags: Optional[Dict[str, bool]] = None # namespace -> updated
|
|||||||
_storage_lock: Optional[LockType] = None
|
_storage_lock: Optional[LockType] = None
|
||||||
_internal_lock: Optional[LockType] = None
|
_internal_lock: Optional[LockType] = None
|
||||||
_pipeline_status_lock: Optional[LockType] = None
|
_pipeline_status_lock: Optional[LockType] = None
|
||||||
|
_graph_db_lock: Optional[LockType] = None
|
||||||
|
_data_init_lock: Optional[LockType] = None
|
||||||
|
|
||||||
|
|
||||||
class UnifiedLock(Generic[T]):
|
class UnifiedLock(Generic[T]):
|
||||||
"""Provide a unified lock interface type for asyncio.Lock and multiprocessing.Lock"""
|
"""Provide a unified lock interface type for asyncio.Lock and multiprocessing.Lock"""
|
||||||
|
|
||||||
def __init__(self, lock: Union[ProcessLock, asyncio.Lock], is_async: bool):
|
def __init__(
|
||||||
|
self,
|
||||||
|
lock: Union[ProcessLock, asyncio.Lock],
|
||||||
|
is_async: bool,
|
||||||
|
name: str = "unnamed",
|
||||||
|
enable_logging: bool = True,
|
||||||
|
):
|
||||||
self._lock = lock
|
self._lock = lock
|
||||||
self._is_async = is_async
|
self._is_async = is_async
|
||||||
|
self._pid = os.getpid() # for debug only
|
||||||
|
self._name = name # for debug only
|
||||||
|
self._enable_logging = enable_logging # for debug only
|
||||||
|
|
||||||
async def __aenter__(self) -> "UnifiedLock[T]":
|
async def __aenter__(self) -> "UnifiedLock[T]":
|
||||||
|
try:
|
||||||
|
direct_log(
|
||||||
|
f"== Lock == Process {self._pid}: Acquiring lock '{self._name}' (async={self._is_async})",
|
||||||
|
enable_output=self._enable_logging,
|
||||||
|
)
|
||||||
if self._is_async:
|
if self._is_async:
|
||||||
await self._lock.acquire()
|
await self._lock.acquire()
|
||||||
else:
|
else:
|
||||||
self._lock.acquire()
|
self._lock.acquire()
|
||||||
|
direct_log(
|
||||||
|
f"== Lock == Process {self._pid}: Lock '{self._name}' acquired (async={self._is_async})",
|
||||||
|
enable_output=self._enable_logging,
|
||||||
|
)
|
||||||
return self
|
return self
|
||||||
|
except Exception as e:
|
||||||
|
direct_log(
|
||||||
|
f"== Lock == Process {self._pid}: Failed to acquire lock '{self._name}': {e}",
|
||||||
|
level="ERROR",
|
||||||
|
enable_output=self._enable_logging,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
try:
|
||||||
|
direct_log(
|
||||||
|
f"== Lock == Process {self._pid}: Releasing lock '{self._name}' (async={self._is_async})",
|
||||||
|
enable_output=self._enable_logging,
|
||||||
|
)
|
||||||
if self._is_async:
|
if self._is_async:
|
||||||
self._lock.release()
|
self._lock.release()
|
||||||
else:
|
else:
|
||||||
self._lock.release()
|
self._lock.release()
|
||||||
|
direct_log(
|
||||||
|
f"== Lock == Process {self._pid}: Lock '{self._name}' released (async={self._is_async})",
|
||||||
|
enable_output=self._enable_logging,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
direct_log(
|
||||||
|
f"== Lock == Process {self._pid}: Failed to release lock '{self._name}': {e}",
|
||||||
|
level="ERROR",
|
||||||
|
enable_output=self._enable_logging,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
def __enter__(self) -> "UnifiedLock[T]":
|
def __enter__(self) -> "UnifiedLock[T]":
|
||||||
"""For backward compatibility"""
|
"""For backward compatibility"""
|
||||||
|
try:
|
||||||
if self._is_async:
|
if self._is_async:
|
||||||
raise RuntimeError("Use 'async with' for shared_storage lock")
|
raise RuntimeError("Use 'async with' for shared_storage lock")
|
||||||
|
direct_log(
|
||||||
|
f"== Lock == Process {self._pid}: Acquiring lock '{self._name}' (sync)",
|
||||||
|
enable_output=self._enable_logging,
|
||||||
|
)
|
||||||
self._lock.acquire()
|
self._lock.acquire()
|
||||||
|
direct_log(
|
||||||
|
f"== Lock == Process {self._pid}: Lock '{self._name}' acquired (sync)",
|
||||||
|
enable_output=self._enable_logging,
|
||||||
|
)
|
||||||
return self
|
return self
|
||||||
|
except Exception as e:
|
||||||
|
direct_log(
|
||||||
|
f"== Lock == Process {self._pid}: Failed to acquire lock '{self._name}' (sync): {e}",
|
||||||
|
level="ERROR",
|
||||||
|
enable_output=self._enable_logging,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
"""For backward compatibility"""
|
"""For backward compatibility"""
|
||||||
|
try:
|
||||||
if self._is_async:
|
if self._is_async:
|
||||||
raise RuntimeError("Use 'async with' for shared_storage lock")
|
raise RuntimeError("Use 'async with' for shared_storage lock")
|
||||||
|
direct_log(
|
||||||
|
f"== Lock == Process {self._pid}: Releasing lock '{self._name}' (sync)",
|
||||||
|
enable_output=self._enable_logging,
|
||||||
|
)
|
||||||
self._lock.release()
|
self._lock.release()
|
||||||
|
direct_log(
|
||||||
|
f"== Lock == Process {self._pid}: Lock '{self._name}' released (sync)",
|
||||||
|
enable_output=self._enable_logging,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
direct_log(
|
||||||
|
f"== Lock == Process {self._pid}: Failed to release lock '{self._name}' (sync): {e}",
|
||||||
|
level="ERROR",
|
||||||
|
enable_output=self._enable_logging,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def get_internal_lock() -> UnifiedLock:
|
def get_internal_lock(enable_logging: bool = False) -> UnifiedLock:
|
||||||
"""return unified storage lock for data consistency"""
|
"""return unified storage lock for data consistency"""
|
||||||
return UnifiedLock(lock=_internal_lock, is_async=not is_multiprocess)
|
return UnifiedLock(
|
||||||
|
lock=_internal_lock,
|
||||||
|
is_async=not is_multiprocess,
|
||||||
|
name="internal_lock",
|
||||||
|
enable_logging=enable_logging,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_storage_lock() -> UnifiedLock:
|
def get_storage_lock(enable_logging: bool = False) -> UnifiedLock:
|
||||||
"""return unified storage lock for data consistency"""
|
"""return unified storage lock for data consistency"""
|
||||||
return UnifiedLock(lock=_storage_lock, is_async=not is_multiprocess)
|
return UnifiedLock(
|
||||||
|
lock=_storage_lock,
|
||||||
|
is_async=not is_multiprocess,
|
||||||
|
name="storage_lock",
|
||||||
|
enable_logging=enable_logging,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_pipeline_status_lock() -> UnifiedLock:
|
def get_pipeline_status_lock(enable_logging: bool = False) -> UnifiedLock:
|
||||||
"""return unified storage lock for data consistency"""
|
"""return unified storage lock for data consistency"""
|
||||||
return UnifiedLock(lock=_pipeline_status_lock, is_async=not is_multiprocess)
|
return UnifiedLock(
|
||||||
|
lock=_pipeline_status_lock,
|
||||||
|
is_async=not is_multiprocess,
|
||||||
|
name="pipeline_status_lock",
|
||||||
|
enable_logging=enable_logging,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_graph_db_lock(enable_logging: bool = False) -> UnifiedLock:
|
||||||
|
"""return unified graph database lock for ensuring atomic operations"""
|
||||||
|
return UnifiedLock(
|
||||||
|
lock=_graph_db_lock,
|
||||||
|
is_async=not is_multiprocess,
|
||||||
|
name="graph_db_lock",
|
||||||
|
enable_logging=enable_logging,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_data_init_lock(enable_logging: bool = False) -> UnifiedLock:
|
||||||
|
"""return unified data initialization lock for ensuring atomic data initialization"""
|
||||||
|
return UnifiedLock(
|
||||||
|
lock=_data_init_lock,
|
||||||
|
is_async=not is_multiprocess,
|
||||||
|
name="data_init_lock",
|
||||||
|
enable_logging=enable_logging,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def initialize_share_data(workers: int = 1):
|
def initialize_share_data(workers: int = 1):
|
||||||
@@ -108,6 +224,8 @@ def initialize_share_data(workers: int = 1):
|
|||||||
_storage_lock, \
|
_storage_lock, \
|
||||||
_internal_lock, \
|
_internal_lock, \
|
||||||
_pipeline_status_lock, \
|
_pipeline_status_lock, \
|
||||||
|
_graph_db_lock, \
|
||||||
|
_data_init_lock, \
|
||||||
_shared_dicts, \
|
_shared_dicts, \
|
||||||
_init_flags, \
|
_init_flags, \
|
||||||
_initialized, \
|
_initialized, \
|
||||||
@@ -120,14 +238,16 @@ def initialize_share_data(workers: int = 1):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
_manager = Manager()
|
|
||||||
_workers = workers
|
_workers = workers
|
||||||
|
|
||||||
if workers > 1:
|
if workers > 1:
|
||||||
is_multiprocess = True
|
is_multiprocess = True
|
||||||
|
_manager = Manager()
|
||||||
_internal_lock = _manager.Lock()
|
_internal_lock = _manager.Lock()
|
||||||
_storage_lock = _manager.Lock()
|
_storage_lock = _manager.Lock()
|
||||||
_pipeline_status_lock = _manager.Lock()
|
_pipeline_status_lock = _manager.Lock()
|
||||||
|
_graph_db_lock = _manager.Lock()
|
||||||
|
_data_init_lock = _manager.Lock()
|
||||||
_shared_dicts = _manager.dict()
|
_shared_dicts = _manager.dict()
|
||||||
_init_flags = _manager.dict()
|
_init_flags = _manager.dict()
|
||||||
_update_flags = _manager.dict()
|
_update_flags = _manager.dict()
|
||||||
@@ -139,6 +259,8 @@ def initialize_share_data(workers: int = 1):
|
|||||||
_internal_lock = asyncio.Lock()
|
_internal_lock = asyncio.Lock()
|
||||||
_storage_lock = asyncio.Lock()
|
_storage_lock = asyncio.Lock()
|
||||||
_pipeline_status_lock = asyncio.Lock()
|
_pipeline_status_lock = asyncio.Lock()
|
||||||
|
_graph_db_lock = asyncio.Lock()
|
||||||
|
_data_init_lock = asyncio.Lock()
|
||||||
_shared_dicts = {}
|
_shared_dicts = {}
|
||||||
_init_flags = {}
|
_init_flags = {}
|
||||||
_update_flags = {}
|
_update_flags = {}
|
||||||
@@ -164,6 +286,7 @@ async def initialize_pipeline_status():
|
|||||||
history_messages = _manager.list() if is_multiprocess else []
|
history_messages = _manager.list() if is_multiprocess else []
|
||||||
pipeline_namespace.update(
|
pipeline_namespace.update(
|
||||||
{
|
{
|
||||||
|
"autoscanned": False, # Auto-scan started
|
||||||
"busy": False, # Control concurrent processes
|
"busy": False, # Control concurrent processes
|
||||||
"job_name": "Default Job", # Current job name (indexing files/indexing texts)
|
"job_name": "Default Job", # Current job name (indexing files/indexing texts)
|
||||||
"job_start": None, # Job start time
|
"job_start": None, # Job start time
|
||||||
@@ -200,7 +323,12 @@ async def get_update_flag(namespace: str):
|
|||||||
if is_multiprocess and _manager is not None:
|
if is_multiprocess and _manager is not None:
|
||||||
new_update_flag = _manager.Value("b", False)
|
new_update_flag = _manager.Value("b", False)
|
||||||
else:
|
else:
|
||||||
new_update_flag = False
|
# Create a simple mutable object to store boolean value for compatibility with mutiprocess
|
||||||
|
class MutableBoolean:
|
||||||
|
def __init__(self, initial_value=False):
|
||||||
|
self.value = initial_value
|
||||||
|
|
||||||
|
new_update_flag = MutableBoolean(False)
|
||||||
|
|
||||||
_update_flags[namespace].append(new_update_flag)
|
_update_flags[namespace].append(new_update_flag)
|
||||||
return new_update_flag
|
return new_update_flag
|
||||||
@@ -220,7 +348,26 @@ async def set_all_update_flags(namespace: str):
|
|||||||
if is_multiprocess:
|
if is_multiprocess:
|
||||||
_update_flags[namespace][i].value = True
|
_update_flags[namespace][i].value = True
|
||||||
else:
|
else:
|
||||||
_update_flags[namespace][i] = True
|
# Use .value attribute instead of direct assignment
|
||||||
|
_update_flags[namespace][i].value = True
|
||||||
|
|
||||||
|
|
||||||
|
async def clear_all_update_flags(namespace: str):
|
||||||
|
"""Clear all update flag of namespace indicating all workers need to reload data from files"""
|
||||||
|
global _update_flags
|
||||||
|
if _update_flags is None:
|
||||||
|
raise ValueError("Try to create namespace before Shared-Data is initialized")
|
||||||
|
|
||||||
|
async with get_internal_lock():
|
||||||
|
if namespace not in _update_flags:
|
||||||
|
raise ValueError(f"Namespace {namespace} not found in update flags")
|
||||||
|
# Update flags for both modes
|
||||||
|
for i in range(len(_update_flags[namespace])):
|
||||||
|
if is_multiprocess:
|
||||||
|
_update_flags[namespace][i].value = False
|
||||||
|
else:
|
||||||
|
# Use .value attribute instead of direct assignment
|
||||||
|
_update_flags[namespace][i].value = False
|
||||||
|
|
||||||
|
|
||||||
async def get_all_update_flags_status() -> Dict[str, list]:
|
async def get_all_update_flags_status() -> Dict[str, list]:
|
||||||
@@ -247,7 +394,7 @@ async def get_all_update_flags_status() -> Dict[str, list]:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def try_initialize_namespace(namespace: str) -> bool:
|
async def try_initialize_namespace(namespace: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns True if the current worker(process) gets initialization permission for loading data later.
|
Returns True if the current worker(process) gets initialization permission for loading data later.
|
||||||
The worker does not get the permission is prohibited to load data from files.
|
The worker does not get the permission is prohibited to load data from files.
|
||||||
@@ -257,6 +404,7 @@ def try_initialize_namespace(namespace: str) -> bool:
|
|||||||
if _init_flags is None:
|
if _init_flags is None:
|
||||||
raise ValueError("Try to create nanmespace before Shared-Data is initialized")
|
raise ValueError("Try to create nanmespace before Shared-Data is initialized")
|
||||||
|
|
||||||
|
async with get_internal_lock():
|
||||||
if namespace not in _init_flags:
|
if namespace not in _init_flags:
|
||||||
_init_flags[namespace] = True
|
_init_flags[namespace] = True
|
||||||
direct_log(
|
direct_log(
|
||||||
@@ -266,6 +414,7 @@ def try_initialize_namespace(namespace: str) -> bool:
|
|||||||
direct_log(
|
direct_log(
|
||||||
f"Process {os.getpid()} storage namespace already initialized: [{namespace}]"
|
f"Process {os.getpid()} storage namespace already initialized: [{namespace}]"
|
||||||
)
|
)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -304,6 +453,8 @@ def finalize_share_data():
|
|||||||
_storage_lock, \
|
_storage_lock, \
|
||||||
_internal_lock, \
|
_internal_lock, \
|
||||||
_pipeline_status_lock, \
|
_pipeline_status_lock, \
|
||||||
|
_graph_db_lock, \
|
||||||
|
_data_init_lock, \
|
||||||
_shared_dicts, \
|
_shared_dicts, \
|
||||||
_init_flags, \
|
_init_flags, \
|
||||||
_initialized, \
|
_initialized, \
|
||||||
@@ -369,6 +520,8 @@ def finalize_share_data():
|
|||||||
_storage_lock = None
|
_storage_lock = None
|
||||||
_internal_lock = None
|
_internal_lock = None
|
||||||
_pipeline_status_lock = None
|
_pipeline_status_lock = None
|
||||||
|
_graph_db_lock = None
|
||||||
|
_data_init_lock = None
|
||||||
_update_flags = None
|
_update_flags = None
|
||||||
|
|
||||||
direct_log(f"Process {os.getpid()} storage data finalization complete")
|
direct_log(f"Process {os.getpid()} storage data finalization complete")
|
||||||
|
@@ -465,6 +465,100 @@ class TiDBVectorDBStorage(BaseVectorStorage):
|
|||||||
logger.error(f"Error searching records with prefix '{prefix}': {e}")
|
logger.error(f"Error searching records with prefix '{prefix}': {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
async def get_by_id(self, id: str) -> dict[str, Any] | None:
|
||||||
|
"""Get vector data by its ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id: The unique identifier of the vector
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The vector data if found, or None if not found
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Determine which table to query based on namespace
|
||||||
|
if self.namespace == NameSpace.VECTOR_STORE_ENTITIES:
|
||||||
|
sql_template = """
|
||||||
|
SELECT entity_id as id, name as entity_name, entity_type, description, content
|
||||||
|
FROM LIGHTRAG_GRAPH_NODES
|
||||||
|
WHERE entity_id = :entity_id AND workspace = :workspace
|
||||||
|
"""
|
||||||
|
params = {"entity_id": id, "workspace": self.db.workspace}
|
||||||
|
elif self.namespace == NameSpace.VECTOR_STORE_RELATIONSHIPS:
|
||||||
|
sql_template = """
|
||||||
|
SELECT relation_id as id, source_name as src_id, target_name as tgt_id,
|
||||||
|
keywords, description, content
|
||||||
|
FROM LIGHTRAG_GRAPH_EDGES
|
||||||
|
WHERE relation_id = :relation_id AND workspace = :workspace
|
||||||
|
"""
|
||||||
|
params = {"relation_id": id, "workspace": self.db.workspace}
|
||||||
|
elif self.namespace == NameSpace.VECTOR_STORE_CHUNKS:
|
||||||
|
sql_template = """
|
||||||
|
SELECT chunk_id as id, content, tokens, chunk_order_index, full_doc_id
|
||||||
|
FROM LIGHTRAG_DOC_CHUNKS
|
||||||
|
WHERE chunk_id = :chunk_id AND workspace = :workspace
|
||||||
|
"""
|
||||||
|
params = {"chunk_id": id, "workspace": self.db.workspace}
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"Namespace {self.namespace} not supported for get_by_id"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
result = await self.db.query(sql_template, params=params)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving vector data for ID {id}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
|
||||||
|
"""Get multiple vector data by their IDs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ids: List of unique identifiers
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of vector data objects that were found
|
||||||
|
"""
|
||||||
|
if not ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Format IDs for SQL IN clause
|
||||||
|
ids_str = ", ".join([f"'{id}'" for id in ids])
|
||||||
|
|
||||||
|
# Determine which table to query based on namespace
|
||||||
|
if self.namespace == NameSpace.VECTOR_STORE_ENTITIES:
|
||||||
|
sql_template = f"""
|
||||||
|
SELECT entity_id as id, name as entity_name, entity_type, description, content
|
||||||
|
FROM LIGHTRAG_GRAPH_NODES
|
||||||
|
WHERE entity_id IN ({ids_str}) AND workspace = :workspace
|
||||||
|
"""
|
||||||
|
elif self.namespace == NameSpace.VECTOR_STORE_RELATIONSHIPS:
|
||||||
|
sql_template = f"""
|
||||||
|
SELECT relation_id as id, source_name as src_id, target_name as tgt_id,
|
||||||
|
keywords, description, content
|
||||||
|
FROM LIGHTRAG_GRAPH_EDGES
|
||||||
|
WHERE relation_id IN ({ids_str}) AND workspace = :workspace
|
||||||
|
"""
|
||||||
|
elif self.namespace == NameSpace.VECTOR_STORE_CHUNKS:
|
||||||
|
sql_template = f"""
|
||||||
|
SELECT chunk_id as id, content, tokens, chunk_order_index, full_doc_id
|
||||||
|
FROM LIGHTRAG_DOC_CHUNKS
|
||||||
|
WHERE chunk_id IN ({ids_str}) AND workspace = :workspace
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"Namespace {self.namespace} not supported for get_by_ids"
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
|
||||||
|
params = {"workspace": self.db.workspace}
|
||||||
|
results = await self.db.query(sql_template, params=params, multirows=True)
|
||||||
|
return results if results else []
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@@ -30,11 +30,10 @@ from .namespace import NameSpace, make_namespace
|
|||||||
from .operate import (
|
from .operate import (
|
||||||
chunking_by_token_size,
|
chunking_by_token_size,
|
||||||
extract_entities,
|
extract_entities,
|
||||||
extract_keywords_only,
|
|
||||||
kg_query,
|
kg_query,
|
||||||
kg_query_with_keywords,
|
|
||||||
mix_kg_vector_query,
|
mix_kg_vector_query,
|
||||||
naive_query,
|
naive_query,
|
||||||
|
query_with_keywords,
|
||||||
)
|
)
|
||||||
from .prompt import GRAPH_FIELD_SEP, PROMPTS
|
from .prompt import GRAPH_FIELD_SEP, PROMPTS
|
||||||
from .utils import (
|
from .utils import (
|
||||||
@@ -45,6 +44,9 @@ from .utils import (
|
|||||||
encode_string_by_tiktoken,
|
encode_string_by_tiktoken,
|
||||||
lazy_external_import,
|
lazy_external_import,
|
||||||
limit_async_func_call,
|
limit_async_func_call,
|
||||||
|
get_content_summary,
|
||||||
|
clean_text,
|
||||||
|
check_storage_env_vars,
|
||||||
logger,
|
logger,
|
||||||
)
|
)
|
||||||
from .types import KnowledgeGraph
|
from .types import KnowledgeGraph
|
||||||
@@ -309,7 +311,7 @@ class LightRAG:
|
|||||||
# Verify storage implementation compatibility
|
# Verify storage implementation compatibility
|
||||||
verify_storage_implementation(storage_type, storage_name)
|
verify_storage_implementation(storage_type, storage_name)
|
||||||
# Check environment variables
|
# Check environment variables
|
||||||
# self.check_storage_env_vars(storage_name)
|
check_storage_env_vars(storage_name)
|
||||||
|
|
||||||
# Ensure vector_db_storage_cls_kwargs has required fields
|
# Ensure vector_db_storage_cls_kwargs has required fields
|
||||||
self.vector_db_storage_cls_kwargs = {
|
self.vector_db_storage_cls_kwargs = {
|
||||||
@@ -354,6 +356,9 @@ class LightRAG:
|
|||||||
namespace=make_namespace(
|
namespace=make_namespace(
|
||||||
self.namespace_prefix, NameSpace.KV_STORE_LLM_RESPONSE_CACHE
|
self.namespace_prefix, NameSpace.KV_STORE_LLM_RESPONSE_CACHE
|
||||||
),
|
),
|
||||||
|
global_config=asdict(
|
||||||
|
self
|
||||||
|
), # Add global_config to ensure cache works properly
|
||||||
embedding_func=self.embedding_func,
|
embedding_func=self.embedding_func,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -404,18 +409,8 @@ class LightRAG:
|
|||||||
embedding_func=None,
|
embedding_func=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.llm_response_cache and hasattr(
|
# Directly use llm_response_cache, don't create a new object
|
||||||
self.llm_response_cache, "global_config"
|
|
||||||
):
|
|
||||||
hashing_kv = self.llm_response_cache
|
hashing_kv = self.llm_response_cache
|
||||||
else:
|
|
||||||
hashing_kv = self.key_string_value_json_storage_cls( # type: ignore
|
|
||||||
namespace=make_namespace(
|
|
||||||
self.namespace_prefix, NameSpace.KV_STORE_LLM_RESPONSE_CACHE
|
|
||||||
),
|
|
||||||
global_config=asdict(self),
|
|
||||||
embedding_func=self.embedding_func,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.llm_model_func = limit_async_func_call(self.llm_model_max_async)(
|
self.llm_model_func = limit_async_func_call(self.llm_model_max_async)(
|
||||||
partial(
|
partial(
|
||||||
@@ -543,11 +538,6 @@ class LightRAG:
|
|||||||
storage_class = lazy_external_import(import_path, storage_name)
|
storage_class = lazy_external_import(import_path, storage_name)
|
||||||
return storage_class
|
return storage_class
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def clean_text(text: str) -> str:
|
|
||||||
"""Clean text by removing null bytes (0x00) and whitespace"""
|
|
||||||
return text.strip().replace("\x00", "")
|
|
||||||
|
|
||||||
def insert(
|
def insert(
|
||||||
self,
|
self,
|
||||||
input: str | list[str],
|
input: str | list[str],
|
||||||
@@ -590,6 +580,7 @@ class LightRAG:
|
|||||||
split_by_character, split_by_character_only
|
split_by_character, split_by_character_only
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO: deprecated, use insert instead
|
||||||
def insert_custom_chunks(
|
def insert_custom_chunks(
|
||||||
self,
|
self,
|
||||||
full_text: str,
|
full_text: str,
|
||||||
@@ -601,14 +592,15 @@ class LightRAG:
|
|||||||
self.ainsert_custom_chunks(full_text, text_chunks, doc_id)
|
self.ainsert_custom_chunks(full_text, text_chunks, doc_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO: deprecated, use ainsert instead
|
||||||
async def ainsert_custom_chunks(
|
async def ainsert_custom_chunks(
|
||||||
self, full_text: str, text_chunks: list[str], doc_id: str | None = None
|
self, full_text: str, text_chunks: list[str], doc_id: str | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
update_storage = False
|
update_storage = False
|
||||||
try:
|
try:
|
||||||
# Clean input texts
|
# Clean input texts
|
||||||
full_text = self.clean_text(full_text)
|
full_text = clean_text(full_text)
|
||||||
text_chunks = [self.clean_text(chunk) for chunk in text_chunks]
|
text_chunks = [clean_text(chunk) for chunk in text_chunks]
|
||||||
|
|
||||||
# Process cleaned texts
|
# Process cleaned texts
|
||||||
if doc_id is None:
|
if doc_id is None:
|
||||||
@@ -687,7 +679,7 @@ class LightRAG:
|
|||||||
contents = {id_: doc for id_, doc in zip(ids, input)}
|
contents = {id_: doc for id_, doc in zip(ids, input)}
|
||||||
else:
|
else:
|
||||||
# Clean input text and remove duplicates
|
# Clean input text and remove duplicates
|
||||||
input = list(set(self.clean_text(doc) for doc in input))
|
input = list(set(clean_text(doc) for doc in input))
|
||||||
# Generate contents dict of MD5 hash IDs and documents
|
# Generate contents dict of MD5 hash IDs and documents
|
||||||
contents = {compute_mdhash_id(doc, prefix="doc-"): doc for doc in input}
|
contents = {compute_mdhash_id(doc, prefix="doc-"): doc for doc in input}
|
||||||
|
|
||||||
@@ -703,7 +695,7 @@ class LightRAG:
|
|||||||
new_docs: dict[str, Any] = {
|
new_docs: dict[str, Any] = {
|
||||||
id_: {
|
id_: {
|
||||||
"content": content,
|
"content": content,
|
||||||
"content_summary": self._get_content_summary(content),
|
"content_summary": get_content_summary(content),
|
||||||
"content_length": len(content),
|
"content_length": len(content),
|
||||||
"status": DocStatus.PENDING,
|
"status": DocStatus.PENDING,
|
||||||
"created_at": datetime.now().isoformat(),
|
"created_at": datetime.now().isoformat(),
|
||||||
@@ -892,7 +884,9 @@ class LightRAG:
|
|||||||
self.chunks_vdb.upsert(chunks)
|
self.chunks_vdb.upsert(chunks)
|
||||||
)
|
)
|
||||||
entity_relation_task = asyncio.create_task(
|
entity_relation_task = asyncio.create_task(
|
||||||
self._process_entity_relation_graph(chunks)
|
self._process_entity_relation_graph(
|
||||||
|
chunks, pipeline_status, pipeline_status_lock
|
||||||
|
)
|
||||||
)
|
)
|
||||||
full_docs_task = asyncio.create_task(
|
full_docs_task = asyncio.create_task(
|
||||||
self.full_docs.upsert(
|
self.full_docs.upsert(
|
||||||
@@ -1007,21 +1001,27 @@ class LightRAG:
|
|||||||
pipeline_status["latest_message"] = log_message
|
pipeline_status["latest_message"] = log_message
|
||||||
pipeline_status["history_messages"].append(log_message)
|
pipeline_status["history_messages"].append(log_message)
|
||||||
|
|
||||||
async def _process_entity_relation_graph(self, chunk: dict[str, Any]) -> None:
|
async def _process_entity_relation_graph(
|
||||||
|
self, chunk: dict[str, Any], pipeline_status=None, pipeline_status_lock=None
|
||||||
|
) -> None:
|
||||||
try:
|
try:
|
||||||
await extract_entities(
|
await extract_entities(
|
||||||
chunk,
|
chunk,
|
||||||
knowledge_graph_inst=self.chunk_entity_relation_graph,
|
knowledge_graph_inst=self.chunk_entity_relation_graph,
|
||||||
entity_vdb=self.entities_vdb,
|
entity_vdb=self.entities_vdb,
|
||||||
relationships_vdb=self.relationships_vdb,
|
relationships_vdb=self.relationships_vdb,
|
||||||
llm_response_cache=self.llm_response_cache,
|
|
||||||
global_config=asdict(self),
|
global_config=asdict(self),
|
||||||
|
pipeline_status=pipeline_status,
|
||||||
|
pipeline_status_lock=pipeline_status_lock,
|
||||||
|
llm_response_cache=self.llm_response_cache,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to extract entities and relationships")
|
logger.error("Failed to extract entities and relationships")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
async def _insert_done(self) -> None:
|
async def _insert_done(
|
||||||
|
self, pipeline_status=None, pipeline_status_lock=None
|
||||||
|
) -> None:
|
||||||
tasks = [
|
tasks = [
|
||||||
cast(StorageNameSpace, storage_inst).index_done_callback()
|
cast(StorageNameSpace, storage_inst).index_done_callback()
|
||||||
for storage_inst in [ # type: ignore
|
for storage_inst in [ # type: ignore
|
||||||
@@ -1040,10 +1040,8 @@ class LightRAG:
|
|||||||
log_message = "All Insert done"
|
log_message = "All Insert done"
|
||||||
logger.info(log_message)
|
logger.info(log_message)
|
||||||
|
|
||||||
# 获取 pipeline_status 并更新 latest_message 和 history_messages
|
if pipeline_status is not None and pipeline_status_lock is not None:
|
||||||
from lightrag.kg.shared_storage import get_namespace_data
|
async with pipeline_status_lock:
|
||||||
|
|
||||||
pipeline_status = await get_namespace_data("pipeline_status")
|
|
||||||
pipeline_status["latest_message"] = log_message
|
pipeline_status["latest_message"] = log_message
|
||||||
pipeline_status["history_messages"].append(log_message)
|
pipeline_status["history_messages"].append(log_message)
|
||||||
|
|
||||||
@@ -1062,7 +1060,7 @@ class LightRAG:
|
|||||||
all_chunks_data: dict[str, dict[str, str]] = {}
|
all_chunks_data: dict[str, dict[str, str]] = {}
|
||||||
chunk_to_source_map: dict[str, str] = {}
|
chunk_to_source_map: dict[str, str] = {}
|
||||||
for chunk_data in custom_kg.get("chunks", []):
|
for chunk_data in custom_kg.get("chunks", []):
|
||||||
chunk_content = self.clean_text(chunk_data["content"])
|
chunk_content = clean_text(chunk_data["content"])
|
||||||
source_id = chunk_data["source_id"]
|
source_id = chunk_data["source_id"]
|
||||||
tokens = len(
|
tokens = len(
|
||||||
encode_string_by_tiktoken(
|
encode_string_by_tiktoken(
|
||||||
@@ -1260,16 +1258,7 @@ class LightRAG:
|
|||||||
self.text_chunks,
|
self.text_chunks,
|
||||||
param,
|
param,
|
||||||
asdict(self),
|
asdict(self),
|
||||||
hashing_kv=self.llm_response_cache
|
hashing_kv=self.llm_response_cache, # Directly use llm_response_cache
|
||||||
if self.llm_response_cache
|
|
||||||
and hasattr(self.llm_response_cache, "global_config")
|
|
||||||
else self.key_string_value_json_storage_cls(
|
|
||||||
namespace=make_namespace(
|
|
||||||
self.namespace_prefix, NameSpace.KV_STORE_LLM_RESPONSE_CACHE
|
|
||||||
),
|
|
||||||
global_config=asdict(self),
|
|
||||||
embedding_func=self.embedding_func,
|
|
||||||
),
|
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
)
|
)
|
||||||
elif param.mode == "naive":
|
elif param.mode == "naive":
|
||||||
@@ -1279,16 +1268,7 @@ class LightRAG:
|
|||||||
self.text_chunks,
|
self.text_chunks,
|
||||||
param,
|
param,
|
||||||
asdict(self),
|
asdict(self),
|
||||||
hashing_kv=self.llm_response_cache
|
hashing_kv=self.llm_response_cache, # Directly use llm_response_cache
|
||||||
if self.llm_response_cache
|
|
||||||
and hasattr(self.llm_response_cache, "global_config")
|
|
||||||
else self.key_string_value_json_storage_cls(
|
|
||||||
namespace=make_namespace(
|
|
||||||
self.namespace_prefix, NameSpace.KV_STORE_LLM_RESPONSE_CACHE
|
|
||||||
),
|
|
||||||
global_config=asdict(self),
|
|
||||||
embedding_func=self.embedding_func,
|
|
||||||
),
|
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
)
|
)
|
||||||
elif param.mode == "mix":
|
elif param.mode == "mix":
|
||||||
@@ -1301,16 +1281,7 @@ class LightRAG:
|
|||||||
self.text_chunks,
|
self.text_chunks,
|
||||||
param,
|
param,
|
||||||
asdict(self),
|
asdict(self),
|
||||||
hashing_kv=self.llm_response_cache
|
hashing_kv=self.llm_response_cache, # Directly use llm_response_cache
|
||||||
if self.llm_response_cache
|
|
||||||
and hasattr(self.llm_response_cache, "global_config")
|
|
||||||
else self.key_string_value_json_storage_cls(
|
|
||||||
namespace=make_namespace(
|
|
||||||
self.namespace_prefix, NameSpace.KV_STORE_LLM_RESPONSE_CACHE
|
|
||||||
),
|
|
||||||
global_config=asdict(self),
|
|
||||||
embedding_func=self.embedding_func,
|
|
||||||
),
|
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -1322,8 +1293,17 @@ class LightRAG:
|
|||||||
self, query: str, prompt: str, param: QueryParam = QueryParam()
|
self, query: str, prompt: str, param: QueryParam = QueryParam()
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
1. Extract keywords from the 'query' using new function in operate.py.
|
Query with separate keyword extraction step.
|
||||||
2. Then run the standard aquery() flow with the final prompt (formatted_question).
|
|
||||||
|
This method extracts keywords from the query first, then uses them for the query.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: User query
|
||||||
|
prompt: Additional prompt for the query
|
||||||
|
param: Query parameters
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Query response
|
||||||
"""
|
"""
|
||||||
loop = always_get_an_event_loop()
|
loop = always_get_an_event_loop()
|
||||||
return loop.run_until_complete(
|
return loop.run_until_complete(
|
||||||
@@ -1334,100 +1314,29 @@ class LightRAG:
|
|||||||
self, query: str, prompt: str, param: QueryParam = QueryParam()
|
self, query: str, prompt: str, param: QueryParam = QueryParam()
|
||||||
) -> str | AsyncIterator[str]:
|
) -> str | AsyncIterator[str]:
|
||||||
"""
|
"""
|
||||||
1. Calls extract_keywords_only to get HL/LL keywords from 'query'.
|
Async version of query_with_separate_keyword_extraction.
|
||||||
2. Then calls kg_query(...) or naive_query(...), etc. as the main query, while also injecting the newly extracted keywords if needed.
|
|
||||||
|
Args:
|
||||||
|
query: User query
|
||||||
|
prompt: Additional prompt for the query
|
||||||
|
param: Query parameters
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Query response or async iterator
|
||||||
"""
|
"""
|
||||||
# ---------------------
|
response = await query_with_keywords(
|
||||||
# STEP 1: Keyword Extraction
|
query=query,
|
||||||
# ---------------------
|
prompt=prompt,
|
||||||
hl_keywords, ll_keywords = await extract_keywords_only(
|
|
||||||
text=query,
|
|
||||||
param=param,
|
param=param,
|
||||||
|
knowledge_graph_inst=self.chunk_entity_relation_graph,
|
||||||
|
entities_vdb=self.entities_vdb,
|
||||||
|
relationships_vdb=self.relationships_vdb,
|
||||||
|
chunks_vdb=self.chunks_vdb,
|
||||||
|
text_chunks_db=self.text_chunks,
|
||||||
global_config=asdict(self),
|
global_config=asdict(self),
|
||||||
hashing_kv=self.llm_response_cache
|
hashing_kv=self.llm_response_cache,
|
||||||
or self.key_string_value_json_storage_cls(
|
|
||||||
namespace=make_namespace(
|
|
||||||
self.namespace_prefix, NameSpace.KV_STORE_LLM_RESPONSE_CACHE
|
|
||||||
),
|
|
||||||
global_config=asdict(self),
|
|
||||||
embedding_func=self.embedding_func,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
param.hl_keywords = hl_keywords
|
|
||||||
param.ll_keywords = ll_keywords
|
|
||||||
|
|
||||||
# ---------------------
|
|
||||||
# STEP 2: Final Query Logic
|
|
||||||
# ---------------------
|
|
||||||
|
|
||||||
# Create a new string with the prompt and the keywords
|
|
||||||
ll_keywords_str = ", ".join(ll_keywords)
|
|
||||||
hl_keywords_str = ", ".join(hl_keywords)
|
|
||||||
formatted_question = f"{prompt}\n\n### Keywords:\nHigh-level: {hl_keywords_str}\nLow-level: {ll_keywords_str}\n\n### Query:\n{query}"
|
|
||||||
|
|
||||||
if param.mode in ["local", "global", "hybrid"]:
|
|
||||||
response = await kg_query_with_keywords(
|
|
||||||
formatted_question,
|
|
||||||
self.chunk_entity_relation_graph,
|
|
||||||
self.entities_vdb,
|
|
||||||
self.relationships_vdb,
|
|
||||||
self.text_chunks,
|
|
||||||
param,
|
|
||||||
asdict(self),
|
|
||||||
hashing_kv=self.llm_response_cache
|
|
||||||
if self.llm_response_cache
|
|
||||||
and hasattr(self.llm_response_cache, "global_config")
|
|
||||||
else self.key_string_value_json_storage_cls(
|
|
||||||
namespace=make_namespace(
|
|
||||||
self.namespace_prefix, NameSpace.KV_STORE_LLM_RESPONSE_CACHE
|
|
||||||
),
|
|
||||||
global_config=asdict(self),
|
|
||||||
embedding_func=self.embedding_func,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
elif param.mode == "naive":
|
|
||||||
response = await naive_query(
|
|
||||||
formatted_question,
|
|
||||||
self.chunks_vdb,
|
|
||||||
self.text_chunks,
|
|
||||||
param,
|
|
||||||
asdict(self),
|
|
||||||
hashing_kv=self.llm_response_cache
|
|
||||||
if self.llm_response_cache
|
|
||||||
and hasattr(self.llm_response_cache, "global_config")
|
|
||||||
else self.key_string_value_json_storage_cls(
|
|
||||||
namespace=make_namespace(
|
|
||||||
self.namespace_prefix, NameSpace.KV_STORE_LLM_RESPONSE_CACHE
|
|
||||||
),
|
|
||||||
global_config=asdict(self),
|
|
||||||
embedding_func=self.embedding_func,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
elif param.mode == "mix":
|
|
||||||
response = await mix_kg_vector_query(
|
|
||||||
formatted_question,
|
|
||||||
self.chunk_entity_relation_graph,
|
|
||||||
self.entities_vdb,
|
|
||||||
self.relationships_vdb,
|
|
||||||
self.chunks_vdb,
|
|
||||||
self.text_chunks,
|
|
||||||
param,
|
|
||||||
asdict(self),
|
|
||||||
hashing_kv=self.llm_response_cache
|
|
||||||
if self.llm_response_cache
|
|
||||||
and hasattr(self.llm_response_cache, "global_config")
|
|
||||||
else self.key_string_value_json_storage_cls(
|
|
||||||
namespace=make_namespace(
|
|
||||||
self.namespace_prefix, NameSpace.KV_STORE_LLM_RESPONSE_CACHE
|
|
||||||
),
|
|
||||||
global_config=asdict(self),
|
|
||||||
embedding_func=self.embedding_func,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown mode {param.mode}")
|
|
||||||
|
|
||||||
await self._query_done()
|
await self._query_done()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -1525,21 +1434,6 @@ class LightRAG:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_content_summary(self, content: str, max_length: int = 100) -> str:
|
|
||||||
"""Get summary of document content
|
|
||||||
|
|
||||||
Args:
|
|
||||||
content: Original document content
|
|
||||||
max_length: Maximum length of summary
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Truncated content with ellipsis if needed
|
|
||||||
"""
|
|
||||||
content = content.strip()
|
|
||||||
if len(content) <= max_length:
|
|
||||||
return content
|
|
||||||
return content[:max_length] + "..."
|
|
||||||
|
|
||||||
async def get_processing_status(self) -> dict[str, int]:
|
async def get_processing_status(self) -> dict[str, int]:
|
||||||
"""Get current document processing status counts
|
"""Get current document processing status counts
|
||||||
|
|
||||||
@@ -1816,19 +1710,7 @@ class LightRAG:
|
|||||||
async def get_entity_info(
|
async def get_entity_info(
|
||||||
self, entity_name: str, include_vector_data: bool = False
|
self, entity_name: str, include_vector_data: bool = False
|
||||||
) -> dict[str, str | None | dict[str, str]]:
|
) -> dict[str, str | None | dict[str, str]]:
|
||||||
"""Get detailed information of an entity
|
"""Get detailed information of an entity"""
|
||||||
|
|
||||||
Args:
|
|
||||||
entity_name: Entity name (no need for quotes)
|
|
||||||
include_vector_data: Whether to include data from the vector database
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: A dictionary containing entity information, including:
|
|
||||||
- entity_name: Entity name
|
|
||||||
- source_id: Source document ID
|
|
||||||
- graph_data: Complete node data from the graph database
|
|
||||||
- vector_data: (optional) Data from the vector database
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Get information from the graph
|
# Get information from the graph
|
||||||
node_data = await self.chunk_entity_relation_graph.get_node(entity_name)
|
node_data = await self.chunk_entity_relation_graph.get_node(entity_name)
|
||||||
@@ -1843,29 +1725,15 @@ class LightRAG:
|
|||||||
# Optional: Get vector database information
|
# Optional: Get vector database information
|
||||||
if include_vector_data:
|
if include_vector_data:
|
||||||
entity_id = compute_mdhash_id(entity_name, prefix="ent-")
|
entity_id = compute_mdhash_id(entity_name, prefix="ent-")
|
||||||
vector_data = self.entities_vdb._client.get([entity_id])
|
vector_data = await self.entities_vdb.get_by_id(entity_id)
|
||||||
result["vector_data"] = vector_data[0] if vector_data else None
|
result["vector_data"] = vector_data
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def get_relation_info(
|
async def get_relation_info(
|
||||||
self, src_entity: str, tgt_entity: str, include_vector_data: bool = False
|
self, src_entity: str, tgt_entity: str, include_vector_data: bool = False
|
||||||
) -> dict[str, str | None | dict[str, str]]:
|
) -> dict[str, str | None | dict[str, str]]:
|
||||||
"""Get detailed information of a relationship
|
"""Get detailed information of a relationship"""
|
||||||
|
|
||||||
Args:
|
|
||||||
src_entity: Source entity name (no need for quotes)
|
|
||||||
tgt_entity: Target entity name (no need for quotes)
|
|
||||||
include_vector_data: Whether to include data from the vector database
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: A dictionary containing relationship information, including:
|
|
||||||
- src_entity: Source entity name
|
|
||||||
- tgt_entity: Target entity name
|
|
||||||
- source_id: Source document ID
|
|
||||||
- graph_data: Complete edge data from the graph database
|
|
||||||
- vector_data: (optional) Data from the vector database
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Get information from the graph
|
# Get information from the graph
|
||||||
edge_data = await self.chunk_entity_relation_graph.get_edge(
|
edge_data = await self.chunk_entity_relation_graph.get_edge(
|
||||||
@@ -1883,8 +1751,8 @@ class LightRAG:
|
|||||||
# Optional: Get vector database information
|
# Optional: Get vector database information
|
||||||
if include_vector_data:
|
if include_vector_data:
|
||||||
rel_id = compute_mdhash_id(src_entity + tgt_entity, prefix="rel-")
|
rel_id = compute_mdhash_id(src_entity + tgt_entity, prefix="rel-")
|
||||||
vector_data = self.relationships_vdb._client.get([rel_id])
|
vector_data = await self.relationships_vdb.get_by_id(rel_id)
|
||||||
result["vector_data"] = vector_data[0] if vector_data else None
|
result["vector_data"] = vector_data
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -2682,6 +2550,12 @@ class LightRAG:
|
|||||||
|
|
||||||
# 9. Delete source entities
|
# 9. Delete source entities
|
||||||
for entity_name in source_entities:
|
for entity_name in source_entities:
|
||||||
|
if entity_name == target_entity:
|
||||||
|
logger.info(
|
||||||
|
f"Skipping deletion of '{entity_name}' as it's also the target entity"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
# Delete entity node from knowledge graph
|
# Delete entity node from knowledge graph
|
||||||
await self.chunk_entity_relation_graph.delete_node(entity_name)
|
await self.chunk_entity_relation_graph.delete_node(entity_name)
|
||||||
|
|
||||||
|
@@ -55,6 +55,7 @@ async def azure_openai_complete_if_cache(
|
|||||||
|
|
||||||
openai_async_client = AsyncAzureOpenAI(
|
openai_async_client = AsyncAzureOpenAI(
|
||||||
azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
|
azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
|
||||||
|
azure_deployment=model,
|
||||||
api_key=os.getenv("AZURE_OPENAI_API_KEY"),
|
api_key=os.getenv("AZURE_OPENAI_API_KEY"),
|
||||||
api_version=os.getenv("AZURE_OPENAI_API_VERSION"),
|
api_version=os.getenv("AZURE_OPENAI_API_VERSION"),
|
||||||
)
|
)
|
||||||
@@ -136,6 +137,7 @@ async def azure_openai_embed(
|
|||||||
|
|
||||||
openai_async_client = AsyncAzureOpenAI(
|
openai_async_client = AsyncAzureOpenAI(
|
||||||
azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
|
azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
|
||||||
|
azure_deployment=model,
|
||||||
api_key=os.getenv("AZURE_OPENAI_API_KEY"),
|
api_key=os.getenv("AZURE_OPENAI_API_KEY"),
|
||||||
api_version=os.getenv("AZURE_OPENAI_API_VERSION"),
|
api_version=os.getenv("AZURE_OPENAI_API_VERSION"),
|
||||||
)
|
)
|
||||||
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import os
|
||||||
from typing import Any, AsyncIterator
|
from typing import Any, AsyncIterator
|
||||||
from collections import Counter, defaultdict
|
from collections import Counter, defaultdict
|
||||||
|
|
||||||
@@ -140,18 +141,36 @@ async def _handle_single_entity_extraction(
|
|||||||
):
|
):
|
||||||
if len(record_attributes) < 4 or record_attributes[0] != '"entity"':
|
if len(record_attributes) < 4 or record_attributes[0] != '"entity"':
|
||||||
return None
|
return None
|
||||||
# add this record as a node in the G
|
|
||||||
|
# Clean and validate entity name
|
||||||
entity_name = clean_str(record_attributes[1]).strip('"')
|
entity_name = clean_str(record_attributes[1]).strip('"')
|
||||||
if not entity_name.strip():
|
if not entity_name.strip():
|
||||||
|
logger.warning(
|
||||||
|
f"Entity extraction error: empty entity name in: {record_attributes}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Clean and validate entity type
|
||||||
entity_type = clean_str(record_attributes[2]).strip('"')
|
entity_type = clean_str(record_attributes[2]).strip('"')
|
||||||
|
if not entity_type.strip() or entity_type.startswith('("'):
|
||||||
|
logger.warning(
|
||||||
|
f"Entity extraction error: invalid entity type in: {record_attributes}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Clean and validate description
|
||||||
entity_description = clean_str(record_attributes[3]).strip('"')
|
entity_description = clean_str(record_attributes[3]).strip('"')
|
||||||
entity_source_id = chunk_key
|
if not entity_description.strip():
|
||||||
|
logger.warning(
|
||||||
|
f"Entity extraction error: empty description for entity '{entity_name}' of type '{entity_type}'"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
return dict(
|
return dict(
|
||||||
entity_name=entity_name,
|
entity_name=entity_name,
|
||||||
entity_type=entity_type,
|
entity_type=entity_type,
|
||||||
description=entity_description,
|
description=entity_description,
|
||||||
source_id=entity_source_id,
|
source_id=chunk_key,
|
||||||
metadata={"created_at": time.time()},
|
metadata={"created_at": time.time()},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -220,6 +239,7 @@ async def _merge_nodes_then_upsert(
|
|||||||
entity_name, description, global_config
|
entity_name, description, global_config
|
||||||
)
|
)
|
||||||
node_data = dict(
|
node_data = dict(
|
||||||
|
entity_id=entity_name,
|
||||||
entity_type=entity_type,
|
entity_type=entity_type,
|
||||||
description=description,
|
description=description,
|
||||||
source_id=source_id,
|
source_id=source_id,
|
||||||
@@ -301,6 +321,7 @@ async def _merge_edges_then_upsert(
|
|||||||
await knowledge_graph_inst.upsert_node(
|
await knowledge_graph_inst.upsert_node(
|
||||||
need_insert_id,
|
need_insert_id,
|
||||||
node_data={
|
node_data={
|
||||||
|
"entity_id": need_insert_id,
|
||||||
"source_id": source_id,
|
"source_id": source_id,
|
||||||
"description": description,
|
"description": description,
|
||||||
"entity_type": "UNKNOWN",
|
"entity_type": "UNKNOWN",
|
||||||
@@ -337,11 +358,10 @@ async def extract_entities(
|
|||||||
entity_vdb: BaseVectorStorage,
|
entity_vdb: BaseVectorStorage,
|
||||||
relationships_vdb: BaseVectorStorage,
|
relationships_vdb: BaseVectorStorage,
|
||||||
global_config: dict[str, str],
|
global_config: dict[str, str],
|
||||||
|
pipeline_status: dict = None,
|
||||||
|
pipeline_status_lock=None,
|
||||||
llm_response_cache: BaseKVStorage | None = None,
|
llm_response_cache: BaseKVStorage | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
from lightrag.kg.shared_storage import get_namespace_data
|
|
||||||
|
|
||||||
pipeline_status = await get_namespace_data("pipeline_status")
|
|
||||||
use_llm_func: callable = global_config["llm_model_func"]
|
use_llm_func: callable = global_config["llm_model_func"]
|
||||||
entity_extract_max_gleaning = global_config["entity_extract_max_gleaning"]
|
entity_extract_max_gleaning = global_config["entity_extract_max_gleaning"]
|
||||||
enable_llm_cache_for_entity_extract: bool = global_config[
|
enable_llm_cache_for_entity_extract: bool = global_config[
|
||||||
@@ -400,6 +420,7 @@ async def extract_entities(
|
|||||||
else:
|
else:
|
||||||
_prompt = input_text
|
_prompt = input_text
|
||||||
|
|
||||||
|
# TODO: add cache_type="extract"
|
||||||
arg_hash = compute_args_hash(_prompt)
|
arg_hash = compute_args_hash(_prompt)
|
||||||
cached_return, _1, _2, _3 = await handle_cache(
|
cached_return, _1, _2, _3 = await handle_cache(
|
||||||
llm_response_cache,
|
llm_response_cache,
|
||||||
@@ -407,7 +428,6 @@ async def extract_entities(
|
|||||||
_prompt,
|
_prompt,
|
||||||
"default",
|
"default",
|
||||||
cache_type="extract",
|
cache_type="extract",
|
||||||
force_llm_cache=True,
|
|
||||||
)
|
)
|
||||||
if cached_return:
|
if cached_return:
|
||||||
logger.debug(f"Found cache for {arg_hash}")
|
logger.debug(f"Found cache for {arg_hash}")
|
||||||
@@ -436,47 +456,22 @@ async def extract_entities(
|
|||||||
else:
|
else:
|
||||||
return await use_llm_func(input_text)
|
return await use_llm_func(input_text)
|
||||||
|
|
||||||
async def _process_single_content(chunk_key_dp: tuple[str, TextChunkSchema]):
|
async def _process_extraction_result(result: str, chunk_key: str):
|
||||||
""" "Prpocess a single chunk
|
"""Process a single extraction result (either initial or gleaning)
|
||||||
Args:
|
Args:
|
||||||
chunk_key_dp (tuple[str, TextChunkSchema]):
|
result (str): The extraction result to process
|
||||||
("chunck-xxxxxx", {"tokens": int, "content": str, "full_doc_id": str, "chunk_order_index": int})
|
chunk_key (str): The chunk key for source tracking
|
||||||
|
Returns:
|
||||||
|
tuple: (nodes_dict, edges_dict) containing the extracted entities and relationships
|
||||||
"""
|
"""
|
||||||
nonlocal processed_chunks
|
maybe_nodes = defaultdict(list)
|
||||||
chunk_key = chunk_key_dp[0]
|
maybe_edges = defaultdict(list)
|
||||||
chunk_dp = chunk_key_dp[1]
|
|
||||||
content = chunk_dp["content"]
|
|
||||||
# hint_prompt = entity_extract_prompt.format(**context_base, input_text=content)
|
|
||||||
hint_prompt = entity_extract_prompt.format(
|
|
||||||
**context_base, input_text="{input_text}"
|
|
||||||
).format(**context_base, input_text=content)
|
|
||||||
|
|
||||||
final_result = await _user_llm_func_with_cache(hint_prompt)
|
|
||||||
history = pack_user_ass_to_openai_messages(hint_prompt, final_result)
|
|
||||||
for now_glean_index in range(entity_extract_max_gleaning):
|
|
||||||
glean_result = await _user_llm_func_with_cache(
|
|
||||||
continue_prompt, history_messages=history
|
|
||||||
)
|
|
||||||
|
|
||||||
history += pack_user_ass_to_openai_messages(continue_prompt, glean_result)
|
|
||||||
final_result += glean_result
|
|
||||||
if now_glean_index == entity_extract_max_gleaning - 1:
|
|
||||||
break
|
|
||||||
|
|
||||||
if_loop_result: str = await _user_llm_func_with_cache(
|
|
||||||
if_loop_prompt, history_messages=history
|
|
||||||
)
|
|
||||||
if_loop_result = if_loop_result.strip().strip('"').strip("'").lower()
|
|
||||||
if if_loop_result != "yes":
|
|
||||||
break
|
|
||||||
|
|
||||||
records = split_string_by_multi_markers(
|
records = split_string_by_multi_markers(
|
||||||
final_result,
|
result,
|
||||||
[context_base["record_delimiter"], context_base["completion_delimiter"]],
|
[context_base["record_delimiter"], context_base["completion_delimiter"]],
|
||||||
)
|
)
|
||||||
|
|
||||||
maybe_nodes = defaultdict(list)
|
|
||||||
maybe_edges = defaultdict(list)
|
|
||||||
for record in records:
|
for record in records:
|
||||||
record = re.search(r"\((.*)\)", record)
|
record = re.search(r"\((.*)\)", record)
|
||||||
if record is None:
|
if record is None:
|
||||||
@@ -485,6 +480,7 @@ async def extract_entities(
|
|||||||
record_attributes = split_string_by_multi_markers(
|
record_attributes = split_string_by_multi_markers(
|
||||||
record, [context_base["tuple_delimiter"]]
|
record, [context_base["tuple_delimiter"]]
|
||||||
)
|
)
|
||||||
|
|
||||||
if_entities = await _handle_single_entity_extraction(
|
if_entities = await _handle_single_entity_extraction(
|
||||||
record_attributes, chunk_key
|
record_attributes, chunk_key
|
||||||
)
|
)
|
||||||
@@ -499,11 +495,69 @@ async def extract_entities(
|
|||||||
maybe_edges[(if_relation["src_id"], if_relation["tgt_id"])].append(
|
maybe_edges[(if_relation["src_id"], if_relation["tgt_id"])].append(
|
||||||
if_relation
|
if_relation
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return maybe_nodes, maybe_edges
|
||||||
|
|
||||||
|
async def _process_single_content(chunk_key_dp: tuple[str, TextChunkSchema]):
|
||||||
|
"""Process a single chunk
|
||||||
|
Args:
|
||||||
|
chunk_key_dp (tuple[str, TextChunkSchema]):
|
||||||
|
("chunk-xxxxxx", {"tokens": int, "content": str, "full_doc_id": str, "chunk_order_index": int})
|
||||||
|
"""
|
||||||
|
nonlocal processed_chunks
|
||||||
|
chunk_key = chunk_key_dp[0]
|
||||||
|
chunk_dp = chunk_key_dp[1]
|
||||||
|
content = chunk_dp["content"]
|
||||||
|
|
||||||
|
# Get initial extraction
|
||||||
|
hint_prompt = entity_extract_prompt.format(
|
||||||
|
**context_base, input_text="{input_text}"
|
||||||
|
).format(**context_base, input_text=content)
|
||||||
|
|
||||||
|
final_result = await _user_llm_func_with_cache(hint_prompt)
|
||||||
|
history = pack_user_ass_to_openai_messages(hint_prompt, final_result)
|
||||||
|
|
||||||
|
# Process initial extraction
|
||||||
|
maybe_nodes, maybe_edges = await _process_extraction_result(
|
||||||
|
final_result, chunk_key
|
||||||
|
)
|
||||||
|
|
||||||
|
# Process additional gleaning results
|
||||||
|
for now_glean_index in range(entity_extract_max_gleaning):
|
||||||
|
glean_result = await _user_llm_func_with_cache(
|
||||||
|
continue_prompt, history_messages=history
|
||||||
|
)
|
||||||
|
|
||||||
|
history += pack_user_ass_to_openai_messages(continue_prompt, glean_result)
|
||||||
|
|
||||||
|
# Process gleaning result separately
|
||||||
|
glean_nodes, glean_edges = await _process_extraction_result(
|
||||||
|
glean_result, chunk_key
|
||||||
|
)
|
||||||
|
|
||||||
|
# Merge results
|
||||||
|
for entity_name, entities in glean_nodes.items():
|
||||||
|
maybe_nodes[entity_name].extend(entities)
|
||||||
|
for edge_key, edges in glean_edges.items():
|
||||||
|
maybe_edges[edge_key].extend(edges)
|
||||||
|
|
||||||
|
if now_glean_index == entity_extract_max_gleaning - 1:
|
||||||
|
break
|
||||||
|
|
||||||
|
if_loop_result: str = await _user_llm_func_with_cache(
|
||||||
|
if_loop_prompt, history_messages=history
|
||||||
|
)
|
||||||
|
if_loop_result = if_loop_result.strip().strip('"').strip("'").lower()
|
||||||
|
if if_loop_result != "yes":
|
||||||
|
break
|
||||||
|
|
||||||
processed_chunks += 1
|
processed_chunks += 1
|
||||||
entities_count = len(maybe_nodes)
|
entities_count = len(maybe_nodes)
|
||||||
relations_count = len(maybe_edges)
|
relations_count = len(maybe_edges)
|
||||||
log_message = f" Chunk {processed_chunks}/{total_chunks}: extracted {entities_count} entities and {relations_count} relationships (deduplicated)"
|
log_message = f" Chunk {processed_chunks}/{total_chunks}: extracted {entities_count} entities and {relations_count} relationships (deduplicated)"
|
||||||
logger.info(log_message)
|
logger.info(log_message)
|
||||||
|
if pipeline_status is not None:
|
||||||
|
async with pipeline_status_lock:
|
||||||
pipeline_status["latest_message"] = log_message
|
pipeline_status["latest_message"] = log_message
|
||||||
pipeline_status["history_messages"].append(log_message)
|
pipeline_status["history_messages"].append(log_message)
|
||||||
return dict(maybe_nodes), dict(maybe_edges)
|
return dict(maybe_nodes), dict(maybe_edges)
|
||||||
@@ -519,6 +573,12 @@ async def extract_entities(
|
|||||||
for k, v in m_edges.items():
|
for k, v in m_edges.items():
|
||||||
maybe_edges[tuple(sorted(k))].extend(v)
|
maybe_edges[tuple(sorted(k))].extend(v)
|
||||||
|
|
||||||
|
from .kg.shared_storage import get_graph_db_lock
|
||||||
|
|
||||||
|
graph_db_lock = get_graph_db_lock(enable_logging=False)
|
||||||
|
|
||||||
|
# Ensure that nodes and edges are merged and upserted atomically
|
||||||
|
async with graph_db_lock:
|
||||||
all_entities_data = await asyncio.gather(
|
all_entities_data = await asyncio.gather(
|
||||||
*[
|
*[
|
||||||
_merge_nodes_then_upsert(k, v, knowledge_graph_inst, global_config)
|
_merge_nodes_then_upsert(k, v, knowledge_graph_inst, global_config)
|
||||||
@@ -528,7 +588,9 @@ async def extract_entities(
|
|||||||
|
|
||||||
all_relationships_data = await asyncio.gather(
|
all_relationships_data = await asyncio.gather(
|
||||||
*[
|
*[
|
||||||
_merge_edges_then_upsert(k[0], k[1], v, knowledge_graph_inst, global_config)
|
_merge_edges_then_upsert(
|
||||||
|
k[0], k[1], v, knowledge_graph_inst, global_config
|
||||||
|
)
|
||||||
for k, v in maybe_edges.items()
|
for k, v in maybe_edges.items()
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -536,6 +598,8 @@ async def extract_entities(
|
|||||||
if not (all_entities_data or all_relationships_data):
|
if not (all_entities_data or all_relationships_data):
|
||||||
log_message = "Didn't extract any entities and relationships."
|
log_message = "Didn't extract any entities and relationships."
|
||||||
logger.info(log_message)
|
logger.info(log_message)
|
||||||
|
if pipeline_status is not None:
|
||||||
|
async with pipeline_status_lock:
|
||||||
pipeline_status["latest_message"] = log_message
|
pipeline_status["latest_message"] = log_message
|
||||||
pipeline_status["history_messages"].append(log_message)
|
pipeline_status["history_messages"].append(log_message)
|
||||||
return
|
return
|
||||||
@@ -543,16 +607,22 @@ async def extract_entities(
|
|||||||
if not all_entities_data:
|
if not all_entities_data:
|
||||||
log_message = "Didn't extract any entities"
|
log_message = "Didn't extract any entities"
|
||||||
logger.info(log_message)
|
logger.info(log_message)
|
||||||
|
if pipeline_status is not None:
|
||||||
|
async with pipeline_status_lock:
|
||||||
pipeline_status["latest_message"] = log_message
|
pipeline_status["latest_message"] = log_message
|
||||||
pipeline_status["history_messages"].append(log_message)
|
pipeline_status["history_messages"].append(log_message)
|
||||||
if not all_relationships_data:
|
if not all_relationships_data:
|
||||||
log_message = "Didn't extract any relationships"
|
log_message = "Didn't extract any relationships"
|
||||||
logger.info(log_message)
|
logger.info(log_message)
|
||||||
|
if pipeline_status is not None:
|
||||||
|
async with pipeline_status_lock:
|
||||||
pipeline_status["latest_message"] = log_message
|
pipeline_status["latest_message"] = log_message
|
||||||
pipeline_status["history_messages"].append(log_message)
|
pipeline_status["history_messages"].append(log_message)
|
||||||
|
|
||||||
log_message = f"Extracted {len(all_entities_data)} entities and {len(all_relationships_data)} relationships (deduplicated)"
|
log_message = f"Extracted {len(all_entities_data)} entities and {len(all_relationships_data)} relationships (deduplicated)"
|
||||||
logger.info(log_message)
|
logger.info(log_message)
|
||||||
|
if pipeline_status is not None:
|
||||||
|
async with pipeline_status_lock:
|
||||||
pipeline_status["latest_message"] = log_message
|
pipeline_status["latest_message"] = log_message
|
||||||
pipeline_status["history_messages"].append(log_message)
|
pipeline_status["history_messages"].append(log_message)
|
||||||
verbose_debug(
|
verbose_debug(
|
||||||
@@ -1020,6 +1090,7 @@ async def _build_query_context(
|
|||||||
text_chunks_db: BaseKVStorage,
|
text_chunks_db: BaseKVStorage,
|
||||||
query_param: QueryParam,
|
query_param: QueryParam,
|
||||||
):
|
):
|
||||||
|
logger.info(f"Process {os.getpid()} buidling query context...")
|
||||||
if query_param.mode == "local":
|
if query_param.mode == "local":
|
||||||
entities_context, relations_context, text_units_context = await _get_node_data(
|
entities_context, relations_context, text_units_context = await _get_node_data(
|
||||||
ll_keywords,
|
ll_keywords,
|
||||||
@@ -1845,3 +1916,90 @@ async def kg_query_with_keywords(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
async def query_with_keywords(
|
||||||
|
query: str,
|
||||||
|
prompt: str,
|
||||||
|
param: QueryParam,
|
||||||
|
knowledge_graph_inst: BaseGraphStorage,
|
||||||
|
entities_vdb: BaseVectorStorage,
|
||||||
|
relationships_vdb: BaseVectorStorage,
|
||||||
|
chunks_vdb: BaseVectorStorage,
|
||||||
|
text_chunks_db: BaseKVStorage,
|
||||||
|
global_config: dict[str, str],
|
||||||
|
hashing_kv: BaseKVStorage | None = None,
|
||||||
|
) -> str | AsyncIterator[str]:
|
||||||
|
"""
|
||||||
|
Extract keywords from the query and then use them for retrieving information.
|
||||||
|
|
||||||
|
1. Extracts high-level and low-level keywords from the query
|
||||||
|
2. Formats the query with the extracted keywords and prompt
|
||||||
|
3. Uses the appropriate query method based on param.mode
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: The user's query
|
||||||
|
prompt: Additional prompt to prepend to the query
|
||||||
|
param: Query parameters
|
||||||
|
knowledge_graph_inst: Knowledge graph storage
|
||||||
|
entities_vdb: Entities vector database
|
||||||
|
relationships_vdb: Relationships vector database
|
||||||
|
chunks_vdb: Document chunks vector database
|
||||||
|
text_chunks_db: Text chunks storage
|
||||||
|
global_config: Global configuration
|
||||||
|
hashing_kv: Cache storage
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Query response or async iterator
|
||||||
|
"""
|
||||||
|
# Extract keywords
|
||||||
|
hl_keywords, ll_keywords = await extract_keywords_only(
|
||||||
|
text=query,
|
||||||
|
param=param,
|
||||||
|
global_config=global_config,
|
||||||
|
hashing_kv=hashing_kv,
|
||||||
|
)
|
||||||
|
|
||||||
|
param.hl_keywords = hl_keywords
|
||||||
|
param.ll_keywords = ll_keywords
|
||||||
|
|
||||||
|
# Create a new string with the prompt and the keywords
|
||||||
|
ll_keywords_str = ", ".join(ll_keywords)
|
||||||
|
hl_keywords_str = ", ".join(hl_keywords)
|
||||||
|
formatted_question = f"{prompt}\n\n### Keywords:\nHigh-level: {hl_keywords_str}\nLow-level: {ll_keywords_str}\n\n### Query:\n{query}"
|
||||||
|
|
||||||
|
# Use appropriate query method based on mode
|
||||||
|
if param.mode in ["local", "global", "hybrid"]:
|
||||||
|
return await kg_query_with_keywords(
|
||||||
|
formatted_question,
|
||||||
|
knowledge_graph_inst,
|
||||||
|
entities_vdb,
|
||||||
|
relationships_vdb,
|
||||||
|
text_chunks_db,
|
||||||
|
param,
|
||||||
|
global_config,
|
||||||
|
hashing_kv=hashing_kv,
|
||||||
|
)
|
||||||
|
elif param.mode == "naive":
|
||||||
|
return await naive_query(
|
||||||
|
formatted_question,
|
||||||
|
chunks_vdb,
|
||||||
|
text_chunks_db,
|
||||||
|
param,
|
||||||
|
global_config,
|
||||||
|
hashing_kv=hashing_kv,
|
||||||
|
)
|
||||||
|
elif param.mode == "mix":
|
||||||
|
return await mix_kg_vector_query(
|
||||||
|
formatted_question,
|
||||||
|
knowledge_graph_inst,
|
||||||
|
entities_vdb,
|
||||||
|
relationships_vdb,
|
||||||
|
chunks_vdb,
|
||||||
|
text_chunks_db,
|
||||||
|
param,
|
||||||
|
global_config,
|
||||||
|
hashing_kv=hashing_kv,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown mode {param.mode}")
|
||||||
|
@@ -236,7 +236,7 @@ Given the query and conversation history, list both high-level and low-level key
|
|||||||
---Instructions---
|
---Instructions---
|
||||||
|
|
||||||
- Consider both the current query and relevant conversation history when extracting keywords
|
- Consider both the current query and relevant conversation history when extracting keywords
|
||||||
- Output the keywords in JSON format
|
- Output the keywords in JSON format, it will be parsed by a JSON parser, do not add any extra content in output
|
||||||
- The JSON should have two keys:
|
- The JSON should have two keys:
|
||||||
- "high_level_keywords" for overarching concepts or themes
|
- "high_level_keywords" for overarching concepts or themes
|
||||||
- "low_level_keywords" for specific entities or details
|
- "low_level_keywords" for specific entities or details
|
||||||
|
@@ -633,15 +633,15 @@ async def handle_cache(
|
|||||||
prompt,
|
prompt,
|
||||||
mode="default",
|
mode="default",
|
||||||
cache_type=None,
|
cache_type=None,
|
||||||
force_llm_cache=False,
|
|
||||||
):
|
):
|
||||||
"""Generic cache handling function"""
|
"""Generic cache handling function"""
|
||||||
if hashing_kv is None or not (
|
if hashing_kv is None:
|
||||||
force_llm_cache or hashing_kv.global_config.get("enable_llm_cache")
|
return None, None, None, None
|
||||||
):
|
|
||||||
|
if mode != "default": # handle cache for all type of query
|
||||||
|
if not hashing_kv.global_config.get("enable_llm_cache"):
|
||||||
return None, None, None, None
|
return None, None, None, None
|
||||||
|
|
||||||
if mode != "default":
|
|
||||||
# Get embedding cache configuration
|
# Get embedding cache configuration
|
||||||
embedding_cache_config = hashing_kv.global_config.get(
|
embedding_cache_config = hashing_kv.global_config.get(
|
||||||
"embedding_cache_config",
|
"embedding_cache_config",
|
||||||
@@ -651,8 +651,7 @@ async def handle_cache(
|
|||||||
use_llm_check = embedding_cache_config.get("use_llm_check", False)
|
use_llm_check = embedding_cache_config.get("use_llm_check", False)
|
||||||
|
|
||||||
quantized = min_val = max_val = None
|
quantized = min_val = max_val = None
|
||||||
if is_embedding_cache_enabled:
|
if is_embedding_cache_enabled: # Use embedding simularity to match cache
|
||||||
# Use embedding cache
|
|
||||||
current_embedding = await hashing_kv.embedding_func([prompt])
|
current_embedding = await hashing_kv.embedding_func([prompt])
|
||||||
llm_model_func = hashing_kv.global_config.get("llm_model_func")
|
llm_model_func = hashing_kv.global_config.get("llm_model_func")
|
||||||
quantized, min_val, max_val = quantize_embedding(current_embedding[0])
|
quantized, min_val, max_val = quantize_embedding(current_embedding[0])
|
||||||
@@ -667,24 +666,29 @@ async def handle_cache(
|
|||||||
cache_type=cache_type,
|
cache_type=cache_type,
|
||||||
)
|
)
|
||||||
if best_cached_response is not None:
|
if best_cached_response is not None:
|
||||||
logger.info(f"Embedding cached hit(mode:{mode} type:{cache_type})")
|
logger.debug(f"Embedding cached hit(mode:{mode} type:{cache_type})")
|
||||||
return best_cached_response, None, None, None
|
return best_cached_response, None, None, None
|
||||||
else:
|
else:
|
||||||
# if caching keyword embedding is enabled, return the quantized embedding for saving it latter
|
# if caching keyword embedding is enabled, return the quantized embedding for saving it latter
|
||||||
logger.info(f"Embedding cached missed(mode:{mode} type:{cache_type})")
|
logger.debug(f"Embedding cached missed(mode:{mode} type:{cache_type})")
|
||||||
return None, quantized, min_val, max_val
|
return None, quantized, min_val, max_val
|
||||||
|
|
||||||
# For default mode or is_embedding_cache_enabled is False, use regular cache
|
else: # handle cache for entity extraction
|
||||||
# default mode is for extract_entities or naive query
|
if not hashing_kv.global_config.get("enable_llm_cache_for_entity_extract"):
|
||||||
|
return None, None, None, None
|
||||||
|
|
||||||
|
# Here is the conditions of code reaching this point:
|
||||||
|
# 1. All query mode: enable_llm_cache is True and embedding simularity is not enabled
|
||||||
|
# 2. Entity extract: enable_llm_cache_for_entity_extract is True
|
||||||
if exists_func(hashing_kv, "get_by_mode_and_id"):
|
if exists_func(hashing_kv, "get_by_mode_and_id"):
|
||||||
mode_cache = await hashing_kv.get_by_mode_and_id(mode, args_hash) or {}
|
mode_cache = await hashing_kv.get_by_mode_and_id(mode, args_hash) or {}
|
||||||
else:
|
else:
|
||||||
mode_cache = await hashing_kv.get_by_id(mode) or {}
|
mode_cache = await hashing_kv.get_by_id(mode) or {}
|
||||||
if args_hash in mode_cache:
|
if args_hash in mode_cache:
|
||||||
logger.info(f"Non-embedding cached hit(mode:{mode} type:{cache_type})")
|
logger.debug(f"Non-embedding cached hit(mode:{mode} type:{cache_type})")
|
||||||
return mode_cache[args_hash]["return"], None, None, None
|
return mode_cache[args_hash]["return"], None, None, None
|
||||||
|
|
||||||
logger.info(f"Non-embedding cached missed(mode:{mode} type:{cache_type})")
|
logger.debug(f"Non-embedding cached missed(mode:{mode} type:{cache_type})")
|
||||||
return None, None, None, None
|
return None, None, None, None
|
||||||
|
|
||||||
|
|
||||||
@@ -701,9 +705,22 @@ class CacheData:
|
|||||||
|
|
||||||
|
|
||||||
async def save_to_cache(hashing_kv, cache_data: CacheData):
|
async def save_to_cache(hashing_kv, cache_data: CacheData):
|
||||||
if hashing_kv is None or hasattr(cache_data.content, "__aiter__"):
|
"""Save data to cache, with improved handling for streaming responses and duplicate content.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hashing_kv: The key-value storage for caching
|
||||||
|
cache_data: The cache data to save
|
||||||
|
"""
|
||||||
|
# Skip if storage is None or content is a streaming response
|
||||||
|
if hashing_kv is None or not cache_data.content:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# If content is a streaming response, don't cache it
|
||||||
|
if hasattr(cache_data.content, "__aiter__"):
|
||||||
|
logger.debug("Streaming response detected, skipping cache")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get existing cache data
|
||||||
if exists_func(hashing_kv, "get_by_mode_and_id"):
|
if exists_func(hashing_kv, "get_by_mode_and_id"):
|
||||||
mode_cache = (
|
mode_cache = (
|
||||||
await hashing_kv.get_by_mode_and_id(cache_data.mode, cache_data.args_hash)
|
await hashing_kv.get_by_mode_and_id(cache_data.mode, cache_data.args_hash)
|
||||||
@@ -712,6 +729,16 @@ async def save_to_cache(hashing_kv, cache_data: CacheData):
|
|||||||
else:
|
else:
|
||||||
mode_cache = await hashing_kv.get_by_id(cache_data.mode) or {}
|
mode_cache = await hashing_kv.get_by_id(cache_data.mode) or {}
|
||||||
|
|
||||||
|
# Check if we already have identical content cached
|
||||||
|
if cache_data.args_hash in mode_cache:
|
||||||
|
existing_content = mode_cache[cache_data.args_hash].get("return")
|
||||||
|
if existing_content == cache_data.content:
|
||||||
|
logger.info(
|
||||||
|
f"Cache content unchanged for {cache_data.args_hash}, skipping update"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update cache with new content
|
||||||
mode_cache[cache_data.args_hash] = {
|
mode_cache[cache_data.args_hash] = {
|
||||||
"return": cache_data.content,
|
"return": cache_data.content,
|
||||||
"cache_type": cache_data.cache_type,
|
"cache_type": cache_data.cache_type,
|
||||||
@@ -726,6 +753,7 @@ async def save_to_cache(hashing_kv, cache_data: CacheData):
|
|||||||
"original_prompt": cache_data.prompt,
|
"original_prompt": cache_data.prompt,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Only upsert if there's actual new content
|
||||||
await hashing_kv.upsert({cache_data.mode: mode_cache})
|
await hashing_kv.upsert({cache_data.mode: mode_cache})
|
||||||
|
|
||||||
|
|
||||||
@@ -862,3 +890,52 @@ def lazy_external_import(module_name: str, class_name: str) -> Callable[..., Any
|
|||||||
return cls(*args, **kwargs)
|
return cls(*args, **kwargs)
|
||||||
|
|
||||||
return import_class
|
return import_class
|
||||||
|
|
||||||
|
|
||||||
|
def get_content_summary(content: str, max_length: int = 100) -> str:
|
||||||
|
"""Get summary of document content
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Original document content
|
||||||
|
max_length: Maximum length of summary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Truncated content with ellipsis if needed
|
||||||
|
"""
|
||||||
|
content = content.strip()
|
||||||
|
if len(content) <= max_length:
|
||||||
|
return content
|
||||||
|
return content[:max_length] + "..."
|
||||||
|
|
||||||
|
|
||||||
|
def clean_text(text: str) -> str:
|
||||||
|
"""Clean text by removing null bytes (0x00) and whitespace
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Input text to clean
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Cleaned text
|
||||||
|
"""
|
||||||
|
return text.strip().replace("\x00", "")
|
||||||
|
|
||||||
|
|
||||||
|
def check_storage_env_vars(storage_name: str) -> None:
|
||||||
|
"""Check if all required environment variables for storage implementation exist
|
||||||
|
|
||||||
|
Args:
|
||||||
|
storage_name: Storage implementation name
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If required environment variables are missing
|
||||||
|
"""
|
||||||
|
from lightrag.kg import STORAGE_ENV_REQUIREMENTS
|
||||||
|
|
||||||
|
required_vars = STORAGE_ENV_REQUIREMENTS.get(storage_name, [])
|
||||||
|
missing_vars = [var for var in required_vars if var not in os.environ]
|
||||||
|
|
||||||
|
if missing_vars:
|
||||||
|
raise ValueError(
|
||||||
|
f"Storage implementation '{storage_name}' requires the following "
|
||||||
|
f"environment variables: {', '.join(missing_vars)}"
|
||||||
|
)
|
||||||
|
@@ -34,11 +34,13 @@
|
|||||||
"cmdk": "^1.0.4",
|
"cmdk": "^1.0.4",
|
||||||
"graphology": "^0.26.0",
|
"graphology": "^0.26.0",
|
||||||
"graphology-generators": "^0.11.2",
|
"graphology-generators": "^0.11.2",
|
||||||
|
"i18next": "^24.2.2",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
"minisearch": "^7.1.2",
|
"minisearch": "^7.1.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-dropzone": "^14.3.6",
|
"react-dropzone": "^14.3.6",
|
||||||
|
"react-i18next": "^15.4.1",
|
||||||
"react-markdown": "^9.1.0",
|
"react-markdown": "^9.1.0",
|
||||||
"react-number-format": "^5.4.3",
|
"react-number-format": "^5.4.3",
|
||||||
"react-syntax-highlighter": "^15.6.1",
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
@@ -765,8 +767,12 @@
|
|||||||
|
|
||||||
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
|
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
|
||||||
|
|
||||||
|
"html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="],
|
||||||
|
|
||||||
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
|
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
|
||||||
|
|
||||||
|
"i18next": ["i18next@24.2.2", "", { "dependencies": { "@babel/runtime": "^7.23.2" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ=="],
|
||||||
|
|
||||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|
||||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||||
@@ -1093,6 +1099,8 @@
|
|||||||
|
|
||||||
"react-dropzone": ["react-dropzone@14.3.6", "", { "dependencies": { "attr-accept": "^2.2.4", "file-selector": "^2.1.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8 || 18.0.0" } }, "sha512-U792j+x0rcwH/U/Slv/OBNU/LGFYbDLHKKiJoPhNaOianayZevCt4Y5S0CraPssH/6/wT6xhKDfzdXUgCBS0HQ=="],
|
"react-dropzone": ["react-dropzone@14.3.6", "", { "dependencies": { "attr-accept": "^2.2.4", "file-selector": "^2.1.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8 || 18.0.0" } }, "sha512-U792j+x0rcwH/U/Slv/OBNU/LGFYbDLHKKiJoPhNaOianayZevCt4Y5S0CraPssH/6/wT6xhKDfzdXUgCBS0HQ=="],
|
||||||
|
|
||||||
|
"react-i18next": ["react-i18next@15.4.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0" } }, "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw=="],
|
||||||
|
|
||||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
|
|
||||||
"react-markdown": ["react-markdown@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw=="],
|
"react-markdown": ["react-markdown@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw=="],
|
||||||
@@ -1271,6 +1279,8 @@
|
|||||||
|
|
||||||
"vite": ["vite@6.1.1", "", { "dependencies": { "esbuild": "^0.24.2", "postcss": "^8.5.2", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA=="],
|
"vite": ["vite@6.1.1", "", { "dependencies": { "esbuild": "^0.24.2", "postcss": "^8.5.2", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA=="],
|
||||||
|
|
||||||
|
"void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
|
||||||
|
|
||||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
||||||
|
@@ -43,11 +43,13 @@
|
|||||||
"cmdk": "^1.0.4",
|
"cmdk": "^1.0.4",
|
||||||
"graphology": "^0.26.0",
|
"graphology": "^0.26.0",
|
||||||
"graphology-generators": "^0.11.2",
|
"graphology-generators": "^0.11.2",
|
||||||
|
"i18next": "^24.2.2",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
"minisearch": "^7.1.2",
|
"minisearch": "^7.1.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-dropzone": "^14.3.6",
|
"react-dropzone": "^14.3.6",
|
||||||
|
"react-i18next": "^15.4.1",
|
||||||
"react-markdown": "^9.1.0",
|
"react-markdown": "^9.1.0",
|
||||||
"react-number-format": "^5.4.3",
|
"react-number-format": "^5.4.3",
|
||||||
"react-syntax-highlighter": "^15.6.1",
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
|
@@ -3,6 +3,7 @@ import useTheme from '@/hooks/useTheme'
|
|||||||
import { MoonIcon, SunIcon } from 'lucide-react'
|
import { MoonIcon, SunIcon } from 'lucide-react'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { controlButtonVariant } from '@/lib/constants'
|
import { controlButtonVariant } from '@/lib/constants'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that toggles the theme between light and dark.
|
* Component that toggles the theme between light and dark.
|
||||||
@@ -11,13 +12,14 @@ export default function ThemeToggle() {
|
|||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
const setLight = useCallback(() => setTheme('light'), [setTheme])
|
const setLight = useCallback(() => setTheme('light'), [setTheme])
|
||||||
const setDark = useCallback(() => setTheme('dark'), [setTheme])
|
const setDark = useCallback(() => setTheme('dark'), [setTheme])
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
if (theme === 'dark') {
|
if (theme === 'dark') {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
onClick={setLight}
|
onClick={setLight}
|
||||||
variant={controlButtonVariant}
|
variant={controlButtonVariant}
|
||||||
tooltip="Switch to light theme"
|
tooltip={t('header.themeToggle.switchToLight')}
|
||||||
size="icon"
|
size="icon"
|
||||||
side="bottom"
|
side="bottom"
|
||||||
>
|
>
|
||||||
@@ -29,7 +31,7 @@ export default function ThemeToggle() {
|
|||||||
<Button
|
<Button
|
||||||
onClick={setDark}
|
onClick={setDark}
|
||||||
variant={controlButtonVariant}
|
variant={controlButtonVariant}
|
||||||
tooltip="Switch to dark theme"
|
tooltip={t('header.themeToggle.switchToDark')}
|
||||||
size="icon"
|
size="icon"
|
||||||
side="bottom"
|
side="bottom"
|
||||||
>
|
>
|
||||||
|
@@ -13,38 +13,40 @@ import { errorMessage } from '@/lib/utils'
|
|||||||
import { clearDocuments } from '@/api/lightrag'
|
import { clearDocuments } from '@/api/lightrag'
|
||||||
|
|
||||||
import { EraserIcon } from 'lucide-react'
|
import { EraserIcon } from 'lucide-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export default function ClearDocumentsDialog() {
|
export default function ClearDocumentsDialog() {
|
||||||
|
const { t } = useTranslation()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
const handleClear = useCallback(async () => {
|
const handleClear = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const result = await clearDocuments()
|
const result = await clearDocuments()
|
||||||
if (result.status === 'success') {
|
if (result.status === 'success') {
|
||||||
toast.success('Documents cleared successfully')
|
toast.success(t('documentPanel.clearDocuments.success'))
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
} else {
|
} else {
|
||||||
toast.error(`Clear Documents Failed:\n${result.message}`)
|
toast.error(t('documentPanel.clearDocuments.failed', { message: result.message }))
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('Clear Documents Failed:\n' + errorMessage(err))
|
toast.error(t('documentPanel.clearDocuments.error', { error: errorMessage(err) }))
|
||||||
}
|
}
|
||||||
}, [setOpen])
|
}, [setOpen])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" side="bottom" tooltip='Clear documents' size="sm">
|
<Button variant="outline" side="bottom" tooltip={t('documentPanel.clearDocuments.tooltip')} size="sm">
|
||||||
<EraserIcon/> Clear
|
<EraserIcon/> {t('documentPanel.clearDocuments.button')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
|
<DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Clear documents</DialogTitle>
|
<DialogTitle>{t('documentPanel.clearDocuments.title')}</DialogTitle>
|
||||||
<DialogDescription>Do you really want to clear all documents?</DialogDescription>
|
<DialogDescription>{t('documentPanel.clearDocuments.confirm')}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Button variant="destructive" onClick={handleClear}>
|
<Button variant="destructive" onClick={handleClear}>
|
||||||
YES
|
{t('documentPanel.clearDocuments.confirmButton')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
@@ -14,8 +14,10 @@ import { errorMessage } from '@/lib/utils'
|
|||||||
import { uploadDocument } from '@/api/lightrag'
|
import { uploadDocument } from '@/api/lightrag'
|
||||||
|
|
||||||
import { UploadIcon } from 'lucide-react'
|
import { UploadIcon } from 'lucide-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export default function UploadDocumentsDialog() {
|
export default function UploadDocumentsDialog() {
|
||||||
|
const { t } = useTranslation()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [isUploading, setIsUploading] = useState(false)
|
const [isUploading, setIsUploading] = useState(false)
|
||||||
const [progresses, setProgresses] = useState<Record<string, number>>({})
|
const [progresses, setProgresses] = useState<Record<string, number>>({})
|
||||||
@@ -29,24 +31,24 @@ export default function UploadDocumentsDialog() {
|
|||||||
filesToUpload.map(async (file) => {
|
filesToUpload.map(async (file) => {
|
||||||
try {
|
try {
|
||||||
const result = await uploadDocument(file, (percentCompleted: number) => {
|
const result = await uploadDocument(file, (percentCompleted: number) => {
|
||||||
console.debug(`Uploading ${file.name}: ${percentCompleted}%`)
|
console.debug(t('documentPanel.uploadDocuments.uploading', { name: file.name, percent: percentCompleted }))
|
||||||
setProgresses((pre) => ({
|
setProgresses((pre) => ({
|
||||||
...pre,
|
...pre,
|
||||||
[file.name]: percentCompleted
|
[file.name]: percentCompleted
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
if (result.status === 'success') {
|
if (result.status === 'success') {
|
||||||
toast.success(`Upload Success:\n${file.name} uploaded successfully`)
|
toast.success(t('documentPanel.uploadDocuments.success', { name: file.name }))
|
||||||
} else {
|
} else {
|
||||||
toast.error(`Upload Failed:\n${file.name}\n${result.message}`)
|
toast.error(t('documentPanel.uploadDocuments.failed', { name: file.name, message: result.message }))
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(`Upload Failed:\n${file.name}\n${errorMessage(err)}`)
|
toast.error(t('documentPanel.uploadDocuments.error', { name: file.name, error: errorMessage(err) }))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('Upload Failed\n' + errorMessage(err))
|
toast.error(t('documentPanel.uploadDocuments.generalError', { error: errorMessage(err) }))
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploading(false)
|
setIsUploading(false)
|
||||||
// setOpen(false)
|
// setOpen(false)
|
||||||
@@ -66,21 +68,21 @@ export default function UploadDocumentsDialog() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="default" side="bottom" tooltip="Upload documents" size="sm">
|
<Button variant="default" side="bottom" tooltip={t('documentPanel.uploadDocuments.tooltip')} size="sm">
|
||||||
<UploadIcon /> Upload
|
<UploadIcon /> {t('documentPanel.uploadDocuments.button')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
|
<DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Upload documents</DialogTitle>
|
<DialogTitle>{t('documentPanel.uploadDocuments.title')}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Drag and drop your documents here or click to browse.
|
{t('documentPanel.uploadDocuments.description')}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<FileUploader
|
<FileUploader
|
||||||
maxFileCount={Infinity}
|
maxFileCount={Infinity}
|
||||||
maxSize={200 * 1024 * 1024}
|
maxSize={200 * 1024 * 1024}
|
||||||
description="supported types: TXT, MD, DOCX, PDF, PPTX, RTF, ODT, EPUB, HTML, HTM, TEX, JSON, XML, YAML, YML, CSV, LOG, CONF, INI, PROPERTIES, SQL, BAT, SH, C, CPP, PY, JAVA, JS, TS, SWIFT, GO, RB, PHP, CSS, SCSS, LESS"
|
description={t('documentPanel.uploadDocuments.fileTypes')}
|
||||||
onUpload={handleDocumentsUpload}
|
onUpload={handleDocumentsUpload}
|
||||||
progresses={progresses}
|
progresses={progresses}
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
|
@@ -2,21 +2,23 @@ import { useFullScreen } from '@react-sigma/core'
|
|||||||
import { MaximizeIcon, MinimizeIcon } from 'lucide-react'
|
import { MaximizeIcon, MinimizeIcon } from 'lucide-react'
|
||||||
import { controlButtonVariant } from '@/lib/constants'
|
import { controlButtonVariant } from '@/lib/constants'
|
||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that toggles full screen mode.
|
* Component that toggles full screen mode.
|
||||||
*/
|
*/
|
||||||
const FullScreenControl = () => {
|
const FullScreenControl = () => {
|
||||||
const { isFullScreen, toggle } = useFullScreen()
|
const { isFullScreen, toggle } = useFullScreen()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isFullScreen ? (
|
{isFullScreen ? (
|
||||||
<Button variant={controlButtonVariant} onClick={toggle} tooltip="Windowed" size="icon">
|
<Button variant={controlButtonVariant} onClick={toggle} tooltip={t('graphPanel.sideBar.fullScreenControl.windowed')} size="icon">
|
||||||
<MinimizeIcon />
|
<MinimizeIcon />
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button variant={controlButtonVariant} onClick={toggle} tooltip="Full Screen" size="icon">
|
<Button variant={controlButtonVariant} onClick={toggle} tooltip={t('graphPanel.sideBar.fullScreenControl.fullScreen')} size="icon">
|
||||||
<MaximizeIcon />
|
<MaximizeIcon />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
@@ -5,6 +5,7 @@ import { useSettingsStore } from '@/stores/settings'
|
|||||||
import { useGraphStore } from '@/stores/graph'
|
import { useGraphStore } from '@/stores/graph'
|
||||||
import { labelListLimit } from '@/lib/constants'
|
import { labelListLimit } from '@/lib/constants'
|
||||||
import MiniSearch from 'minisearch'
|
import MiniSearch from 'minisearch'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const lastGraph: any = {
|
const lastGraph: any = {
|
||||||
graph: null,
|
graph: null,
|
||||||
@@ -13,6 +14,7 @@ const lastGraph: any = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GraphLabels = () => {
|
const GraphLabels = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const label = useSettingsStore.use.queryLabel()
|
const label = useSettingsStore.use.queryLabel()
|
||||||
const graph = useGraphStore.use.sigmaGraph()
|
const graph = useGraphStore.use.sigmaGraph()
|
||||||
|
|
||||||
@@ -69,7 +71,7 @@ const GraphLabels = () => {
|
|||||||
|
|
||||||
return result.length <= labelListLimit
|
return result.length <= labelListLimit
|
||||||
? result
|
? result
|
||||||
: [...result.slice(0, labelListLimit), `And ${result.length - labelListLimit} others`]
|
: [...result.slice(0, labelListLimit), t('graphLabels.andOthers', { count: result.length - labelListLimit })]
|
||||||
},
|
},
|
||||||
[getSearchEngine]
|
[getSearchEngine]
|
||||||
)
|
)
|
||||||
@@ -84,14 +86,14 @@ const GraphLabels = () => {
|
|||||||
className="ml-2"
|
className="ml-2"
|
||||||
triggerClassName="max-h-8"
|
triggerClassName="max-h-8"
|
||||||
searchInputClassName="max-h-8"
|
searchInputClassName="max-h-8"
|
||||||
triggerTooltip="Select query label"
|
triggerTooltip={t('graphPanel.graphLabels.selectTooltip')}
|
||||||
fetcher={fetchData}
|
fetcher={fetchData}
|
||||||
renderOption={(item) => <div>{item}</div>}
|
renderOption={(item) => <div>{item}</div>}
|
||||||
getOptionValue={(item) => item}
|
getOptionValue={(item) => item}
|
||||||
getDisplayValue={(item) => <div>{item}</div>}
|
getDisplayValue={(item) => <div>{item}</div>}
|
||||||
notFound={<div className="py-6 text-center text-sm">No labels found</div>}
|
notFound={<div className="py-6 text-center text-sm">No labels found</div>}
|
||||||
label="Label"
|
label={t('graphPanel.graphLabels.label')}
|
||||||
placeholder="Search labels..."
|
placeholder={t('graphPanel.graphLabels.placeholder')}
|
||||||
value={label !== null ? label : ''}
|
value={label !== null ? label : ''}
|
||||||
onChange={setQueryLabel}
|
onChange={setQueryLabel}
|
||||||
/>
|
/>
|
||||||
|
@@ -9,6 +9,7 @@ import { AsyncSearch } from '@/components/ui/AsyncSearch'
|
|||||||
import { searchResultLimit } from '@/lib/constants'
|
import { searchResultLimit } from '@/lib/constants'
|
||||||
import { useGraphStore } from '@/stores/graph'
|
import { useGraphStore } from '@/stores/graph'
|
||||||
import MiniSearch from 'minisearch'
|
import MiniSearch from 'minisearch'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface OptionItem {
|
interface OptionItem {
|
||||||
id: string
|
id: string
|
||||||
@@ -44,6 +45,7 @@ export const GraphSearchInput = ({
|
|||||||
onFocus?: GraphSearchInputProps['onFocus']
|
onFocus?: GraphSearchInputProps['onFocus']
|
||||||
value?: GraphSearchInputProps['value']
|
value?: GraphSearchInputProps['value']
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const graph = useGraphStore.use.sigmaGraph()
|
const graph = useGraphStore.use.sigmaGraph()
|
||||||
|
|
||||||
const searchEngine = useMemo(() => {
|
const searchEngine = useMemo(() => {
|
||||||
@@ -97,7 +99,7 @@ export const GraphSearchInput = ({
|
|||||||
{
|
{
|
||||||
type: 'message',
|
type: 'message',
|
||||||
id: messageId,
|
id: messageId,
|
||||||
message: `And ${result.length - searchResultLimit} others`
|
message: t('graphPanel.search.message', { count: result.length - searchResultLimit })
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -118,7 +120,7 @@ export const GraphSearchInput = ({
|
|||||||
if (id !== messageId && onFocus) onFocus(id ? { id, type: 'nodes' } : null)
|
if (id !== messageId && onFocus) onFocus(id ? { id, type: 'nodes' } : null)
|
||||||
}}
|
}}
|
||||||
label={'item'}
|
label={'item'}
|
||||||
placeholder="Search nodes..."
|
placeholder={t('graphPanel.search.placeholder')}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -16,6 +16,7 @@ import { controlButtonVariant } from '@/lib/constants'
|
|||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
|
||||||
import { GripIcon, PlayIcon, PauseIcon } from 'lucide-react'
|
import { GripIcon, PlayIcon, PauseIcon } from 'lucide-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
type LayoutName =
|
type LayoutName =
|
||||||
| 'Circular'
|
| 'Circular'
|
||||||
@@ -28,6 +29,7 @@ type LayoutName =
|
|||||||
const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) => {
|
const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) => {
|
||||||
const sigma = useSigma()
|
const sigma = useSigma()
|
||||||
const { stop, start, isRunning } = layout
|
const { stop, start, isRunning } = layout
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init component when Sigma or component settings change.
|
* Init component when Sigma or component settings change.
|
||||||
@@ -61,7 +63,7 @@ const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) =
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => (isRunning ? stop() : start())}
|
onClick={() => (isRunning ? stop() : start())}
|
||||||
tooltip={isRunning ? 'Stop the layout animation' : 'Start the layout animation'}
|
tooltip={isRunning ? t('graphPanel.sideBar.layoutsControl.stopAnimation') : t('graphPanel.sideBar.layoutsControl.startAnimation')}
|
||||||
variant={controlButtonVariant}
|
variant={controlButtonVariant}
|
||||||
>
|
>
|
||||||
{isRunning ? <PauseIcon /> : <PlayIcon />}
|
{isRunning ? <PauseIcon /> : <PlayIcon />}
|
||||||
@@ -74,6 +76,7 @@ const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) =
|
|||||||
*/
|
*/
|
||||||
const LayoutsControl = () => {
|
const LayoutsControl = () => {
|
||||||
const sigma = useSigma()
|
const sigma = useSigma()
|
||||||
|
const { t } = useTranslation()
|
||||||
const [layout, setLayout] = useState<LayoutName>('Circular')
|
const [layout, setLayout] = useState<LayoutName>('Circular')
|
||||||
const [opened, setOpened] = useState<boolean>(false)
|
const [opened, setOpened] = useState<boolean>(false)
|
||||||
|
|
||||||
@@ -149,7 +152,7 @@ const LayoutsControl = () => {
|
|||||||
size="icon"
|
size="icon"
|
||||||
variant={controlButtonVariant}
|
variant={controlButtonVariant}
|
||||||
onClick={() => setOpened((e: boolean) => !e)}
|
onClick={() => setOpened((e: boolean) => !e)}
|
||||||
tooltip="Layout Graph"
|
tooltip={t('graphPanel.sideBar.layoutsControl.layoutGraph')}
|
||||||
>
|
>
|
||||||
<GripIcon />
|
<GripIcon />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -166,7 +169,7 @@ const LayoutsControl = () => {
|
|||||||
key={name}
|
key={name}
|
||||||
className="cursor-pointer text-xs"
|
className="cursor-pointer text-xs"
|
||||||
>
|
>
|
||||||
{name}
|
{t(`graphPanel.sideBar.layoutsControl.layouts.${name}`)}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
|
|||||||
import { useGraphStore, RawNodeType, RawEdgeType } from '@/stores/graph'
|
import { useGraphStore, RawNodeType, RawEdgeType } from '@/stores/graph'
|
||||||
import Text from '@/components/ui/Text'
|
import Text from '@/components/ui/Text'
|
||||||
import useLightragGraph from '@/hooks/useLightragGraph'
|
import useLightragGraph from '@/hooks/useLightragGraph'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that view properties of elements in graph.
|
* Component that view properties of elements in graph.
|
||||||
@@ -147,21 +148,22 @@ const PropertyRow = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NodePropertiesView = ({ node }: { node: NodeType }) => {
|
const NodePropertiesView = ({ node }: { node: NodeType }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label className="text-md pl-1 font-bold tracking-wide text-sky-300">Node</label>
|
<label className="text-md pl-1 font-bold tracking-wide text-sky-300">{t('graphPanel.propertiesView.node.title')}</label>
|
||||||
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
||||||
<PropertyRow name={'Id'} value={node.id} />
|
<PropertyRow name={t('graphPanel.propertiesView.node.id')} value={node.id} />
|
||||||
<PropertyRow
|
<PropertyRow
|
||||||
name={'Labels'}
|
name={t('graphPanel.propertiesView.node.labels')}
|
||||||
value={node.labels.join(', ')}
|
value={node.labels.join(', ')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
useGraphStore.getState().setSelectedNode(node.id, true)
|
useGraphStore.getState().setSelectedNode(node.id, true)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<PropertyRow name={'Degree'} value={node.degree} />
|
<PropertyRow name={t('graphPanel.propertiesView.node.degree')} value={node.degree} />
|
||||||
</div>
|
</div>
|
||||||
<label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">Properties</label>
|
<label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">{t('graphPanel.propertiesView.node.properties')}</label>
|
||||||
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
||||||
{Object.keys(node.properties)
|
{Object.keys(node.properties)
|
||||||
.sort()
|
.sort()
|
||||||
@@ -172,7 +174,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
|
|||||||
{node.relationships.length > 0 && (
|
{node.relationships.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<label className="text-md pl-1 font-bold tracking-wide text-teal-600/90">
|
<label className="text-md pl-1 font-bold tracking-wide text-teal-600/90">
|
||||||
Relationships
|
{t('graphPanel.propertiesView.node.relationships')}
|
||||||
</label>
|
</label>
|
||||||
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
||||||
{node.relationships.map(({ type, id, label }) => {
|
{node.relationships.map(({ type, id, label }) => {
|
||||||
@@ -195,28 +197,29 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
|
const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label className="text-md pl-1 font-bold tracking-wide text-teal-600">Relationship</label>
|
<label className="text-md pl-1 font-bold tracking-wide text-teal-600">{t('graphPanel.propertiesView.edge.title')}</label>
|
||||||
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
||||||
<PropertyRow name={'Id'} value={edge.id} />
|
<PropertyRow name={t('graphPanel.propertiesView.edge.id')} value={edge.id} />
|
||||||
{edge.type && <PropertyRow name={'Type'} value={edge.type} />}
|
{edge.type && <PropertyRow name={t('graphPanel.propertiesView.edge.type')} value={edge.type} />}
|
||||||
<PropertyRow
|
<PropertyRow
|
||||||
name={'Source'}
|
name={t('graphPanel.propertiesView.edge.source')}
|
||||||
value={edge.sourceNode ? edge.sourceNode.labels.join(', ') : edge.source}
|
value={edge.sourceNode ? edge.sourceNode.labels.join(', ') : edge.source}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
useGraphStore.getState().setSelectedNode(edge.source, true)
|
useGraphStore.getState().setSelectedNode(edge.source, true)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<PropertyRow
|
<PropertyRow
|
||||||
name={'Target'}
|
name={t('graphPanel.propertiesView.edge.target')}
|
||||||
value={edge.targetNode ? edge.targetNode.labels.join(', ') : edge.target}
|
value={edge.targetNode ? edge.targetNode.labels.join(', ') : edge.target}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
useGraphStore.getState().setSelectedNode(edge.target, true)
|
useGraphStore.getState().setSelectedNode(edge.target, true)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">Properties</label>
|
<label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">{t('graphPanel.propertiesView.edge.properties')}</label>
|
||||||
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
||||||
{Object.keys(edge.properties)
|
{Object.keys(edge.properties)
|
||||||
.sort()
|
.sort()
|
||||||
|
@@ -10,6 +10,7 @@ import { useSettingsStore } from '@/stores/settings'
|
|||||||
import { useBackendState } from '@/stores/state'
|
import { useBackendState } from '@/stores/state'
|
||||||
|
|
||||||
import { SettingsIcon } from 'lucide-react'
|
import { SettingsIcon } from 'lucide-react'
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays a checkbox with a label.
|
* Component that displays a checkbox with a label.
|
||||||
@@ -204,10 +205,12 @@ export default function Settings() {
|
|||||||
[setTempApiKey]
|
[setTempApiKey]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={opened} onOpenChange={setOpened}>
|
<Popover open={opened} onOpenChange={setOpened}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant={controlButtonVariant} tooltip="Settings" size="icon">
|
<Button variant={controlButtonVariant} tooltip={t("graphPanel.sideBar.settings.settings")} size="icon">
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -221,7 +224,7 @@ export default function Settings() {
|
|||||||
<LabeledCheckBox
|
<LabeledCheckBox
|
||||||
checked={enableHealthCheck}
|
checked={enableHealthCheck}
|
||||||
onCheckedChange={setEnableHealthCheck}
|
onCheckedChange={setEnableHealthCheck}
|
||||||
label="Health Check"
|
label={t("graphPanel.sideBar.settings.healthCheck")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
@@ -229,12 +232,12 @@ export default function Settings() {
|
|||||||
<LabeledCheckBox
|
<LabeledCheckBox
|
||||||
checked={showPropertyPanel}
|
checked={showPropertyPanel}
|
||||||
onCheckedChange={setShowPropertyPanel}
|
onCheckedChange={setShowPropertyPanel}
|
||||||
label="Show Property Panel"
|
label={t("graphPanel.sideBar.settings.showPropertyPanel")}
|
||||||
/>
|
/>
|
||||||
<LabeledCheckBox
|
<LabeledCheckBox
|
||||||
checked={showNodeSearchBar}
|
checked={showNodeSearchBar}
|
||||||
onCheckedChange={setShowNodeSearchBar}
|
onCheckedChange={setShowNodeSearchBar}
|
||||||
label="Show Search Bar"
|
label={t("graphPanel.sideBar.settings.showSearchBar")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
@@ -242,12 +245,12 @@ export default function Settings() {
|
|||||||
<LabeledCheckBox
|
<LabeledCheckBox
|
||||||
checked={showNodeLabel}
|
checked={showNodeLabel}
|
||||||
onCheckedChange={setShowNodeLabel}
|
onCheckedChange={setShowNodeLabel}
|
||||||
label="Show Node Label"
|
label={t("graphPanel.sideBar.settings.showNodeLabel")}
|
||||||
/>
|
/>
|
||||||
<LabeledCheckBox
|
<LabeledCheckBox
|
||||||
checked={enableNodeDrag}
|
checked={enableNodeDrag}
|
||||||
onCheckedChange={setEnableNodeDrag}
|
onCheckedChange={setEnableNodeDrag}
|
||||||
label="Node Draggable"
|
label={t("graphPanel.sideBar.settings.nodeDraggable")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
@@ -255,51 +258,50 @@ export default function Settings() {
|
|||||||
<LabeledCheckBox
|
<LabeledCheckBox
|
||||||
checked={showEdgeLabel}
|
checked={showEdgeLabel}
|
||||||
onCheckedChange={setShowEdgeLabel}
|
onCheckedChange={setShowEdgeLabel}
|
||||||
label="Show Edge Label"
|
label={t("graphPanel.sideBar.settings.showEdgeLabel")}
|
||||||
/>
|
/>
|
||||||
<LabeledCheckBox
|
<LabeledCheckBox
|
||||||
checked={enableHideUnselectedEdges}
|
checked={enableHideUnselectedEdges}
|
||||||
onCheckedChange={setEnableHideUnselectedEdges}
|
onCheckedChange={setEnableHideUnselectedEdges}
|
||||||
label="Hide Unselected Edges"
|
label={t("graphPanel.sideBar.settings.hideUnselectedEdges")}
|
||||||
/>
|
/>
|
||||||
<LabeledCheckBox
|
<LabeledCheckBox
|
||||||
checked={enableEdgeEvents}
|
checked={enableEdgeEvents}
|
||||||
onCheckedChange={setEnableEdgeEvents}
|
onCheckedChange={setEnableEdgeEvents}
|
||||||
label="Edge Events"
|
label={t("graphPanel.sideBar.settings.edgeEvents")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
<LabeledNumberInput
|
<LabeledNumberInput
|
||||||
label="Max Query Depth"
|
label={t("graphPanel.sideBar.settings.maxQueryDepth")}
|
||||||
min={1}
|
min={1}
|
||||||
value={graphQueryMaxDepth}
|
value={graphQueryMaxDepth}
|
||||||
onEditFinished={setGraphQueryMaxDepth}
|
onEditFinished={setGraphQueryMaxDepth}
|
||||||
/>
|
/>
|
||||||
<LabeledNumberInput
|
<LabeledNumberInput
|
||||||
label="Minimum Degree"
|
label={t("graphPanel.sideBar.settings.minDegree")}
|
||||||
min={0}
|
min={0}
|
||||||
value={graphMinDegree}
|
value={graphMinDegree}
|
||||||
onEditFinished={setGraphMinDegree}
|
onEditFinished={setGraphMinDegree}
|
||||||
/>
|
/>
|
||||||
<LabeledNumberInput
|
<LabeledNumberInput
|
||||||
label="Max Layout Iterations"
|
label={t("graphPanel.sideBar.settings.maxLayoutIterations")}
|
||||||
min={1}
|
min={1}
|
||||||
max={20}
|
max={20}
|
||||||
value={graphLayoutMaxIterations}
|
value={graphLayoutMaxIterations}
|
||||||
onEditFinished={setGraphLayoutMaxIterations}
|
onEditFinished={setGraphLayoutMaxIterations}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label className="text-sm font-medium">API Key</label>
|
<label className="text-sm font-medium">{t("graphPanel.sideBar.settings.apiKey")}</label>
|
||||||
<form className="flex h-6 gap-2" onSubmit={(e) => e.preventDefault()}>
|
<form className="flex h-6 gap-2" onSubmit={(e) => e.preventDefault()}>
|
||||||
<div className="w-0 flex-1">
|
<div className="w-0 flex-1">
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
value={tempApiKey}
|
value={tempApiKey}
|
||||||
onChange={handleTempApiKeyChange}
|
onChange={handleTempApiKeyChange}
|
||||||
placeholder="Enter your API key"
|
placeholder={t("graphPanel.sideBar.settings.enterYourAPIkey")}
|
||||||
className="max-h-full w-full min-w-0"
|
className="max-h-full w-full min-w-0"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
@@ -310,7 +312,7 @@ export default function Settings() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="max-h-full shrink-0"
|
className="max-h-full shrink-0"
|
||||||
>
|
>
|
||||||
Save
|
{t("graphPanel.sideBar.settings.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,58 +1,60 @@
|
|||||||
import { LightragStatus } from '@/api/lightrag'
|
import { LightragStatus } from '@/api/lightrag'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const StatusCard = ({ status }: { status: LightragStatus | null }) => {
|
const StatusCard = ({ status }: { status: LightragStatus | null }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return <div className="text-muted-foreground text-sm">Status information unavailable</div>
|
return <div className="text-muted-foreground text-sm">{t('graphPanel.statusCard.unavailable')}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-w-[300px] space-y-3 text-sm">
|
<div className="min-w-[300px] space-y-3 text-sm">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h4 className="font-medium">Storage Info</h4>
|
<h4 className="font-medium">{t('graphPanel.statusCard.storageInfo')}</h4>
|
||||||
<div className="text-muted-foreground grid grid-cols-2 gap-1">
|
<div className="text-muted-foreground grid grid-cols-2 gap-1">
|
||||||
<span>Working Directory:</span>
|
<span>{t('graphPanel.statusCard.workingDirectory')}:</span>
|
||||||
<span className="truncate">{status.working_directory}</span>
|
<span className="truncate">{status.working_directory}</span>
|
||||||
<span>Input Directory:</span>
|
<span>{t('graphPanel.statusCard.inputDirectory')}:</span>
|
||||||
<span className="truncate">{status.input_directory}</span>
|
<span className="truncate">{status.input_directory}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h4 className="font-medium">LLM Configuration</h4>
|
<h4 className="font-medium">{t('graphPanel.statusCard.llmConfig')}</h4>
|
||||||
<div className="text-muted-foreground grid grid-cols-2 gap-1">
|
<div className="text-muted-foreground grid grid-cols-2 gap-1">
|
||||||
<span>LLM Binding:</span>
|
<span>{t('graphPanel.statusCard.llmBinding')}:</span>
|
||||||
<span>{status.configuration.llm_binding}</span>
|
<span>{status.configuration.llm_binding}</span>
|
||||||
<span>LLM Binding Host:</span>
|
<span>{t('graphPanel.statusCard.llmBindingHost')}:</span>
|
||||||
<span>{status.configuration.llm_binding_host}</span>
|
<span>{status.configuration.llm_binding_host}</span>
|
||||||
<span>LLM Model:</span>
|
<span>{t('graphPanel.statusCard.llmModel')}:</span>
|
||||||
<span>{status.configuration.llm_model}</span>
|
<span>{status.configuration.llm_model}</span>
|
||||||
<span>Max Tokens:</span>
|
<span>{t('graphPanel.statusCard.maxTokens')}:</span>
|
||||||
<span>{status.configuration.max_tokens}</span>
|
<span>{status.configuration.max_tokens}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h4 className="font-medium">Embedding Configuration</h4>
|
<h4 className="font-medium">{t('graphPanel.statusCard.embeddingConfig')}</h4>
|
||||||
<div className="text-muted-foreground grid grid-cols-2 gap-1">
|
<div className="text-muted-foreground grid grid-cols-2 gap-1">
|
||||||
<span>Embedding Binding:</span>
|
<span>{t('graphPanel.statusCard.embeddingBinding')}:</span>
|
||||||
<span>{status.configuration.embedding_binding}</span>
|
<span>{status.configuration.embedding_binding}</span>
|
||||||
<span>Embedding Binding Host:</span>
|
<span>{t('graphPanel.statusCard.embeddingBindingHost')}:</span>
|
||||||
<span>{status.configuration.embedding_binding_host}</span>
|
<span>{status.configuration.embedding_binding_host}</span>
|
||||||
<span>Embedding Model:</span>
|
<span>{t('graphPanel.statusCard.embeddingModel')}:</span>
|
||||||
<span>{status.configuration.embedding_model}</span>
|
<span>{status.configuration.embedding_model}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h4 className="font-medium">Storage Configuration</h4>
|
<h4 className="font-medium">{t('graphPanel.statusCard.storageConfig')}</h4>
|
||||||
<div className="text-muted-foreground grid grid-cols-2 gap-1">
|
<div className="text-muted-foreground grid grid-cols-2 gap-1">
|
||||||
<span>KV Storage:</span>
|
<span>{t('graphPanel.statusCard.kvStorage')}:</span>
|
||||||
<span>{status.configuration.kv_storage}</span>
|
<span>{status.configuration.kv_storage}</span>
|
||||||
<span>Doc Status Storage:</span>
|
<span>{t('graphPanel.statusCard.docStatusStorage')}:</span>
|
||||||
<span>{status.configuration.doc_status_storage}</span>
|
<span>{status.configuration.doc_status_storage}</span>
|
||||||
<span>Graph Storage:</span>
|
<span>{t('graphPanel.statusCard.graphStorage')}:</span>
|
||||||
<span>{status.configuration.graph_storage}</span>
|
<span>{status.configuration.graph_storage}</span>
|
||||||
<span>Vector Storage:</span>
|
<span>{t('graphPanel.statusCard.vectorStorage')}:</span>
|
||||||
<span>{status.configuration.vector_storage}</span>
|
<span>{status.configuration.vector_storage}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -3,8 +3,10 @@ import { useBackendState } from '@/stores/state'
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
||||||
import StatusCard from '@/components/graph/StatusCard'
|
import StatusCard from '@/components/graph/StatusCard'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const StatusIndicator = () => {
|
const StatusIndicator = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const health = useBackendState.use.health()
|
const health = useBackendState.use.health()
|
||||||
const lastCheckTime = useBackendState.use.lastCheckTime()
|
const lastCheckTime = useBackendState.use.lastCheckTime()
|
||||||
const status = useBackendState.use.status()
|
const status = useBackendState.use.status()
|
||||||
@@ -33,7 +35,7 @@ const StatusIndicator = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span className="text-muted-foreground text-xs">
|
<span className="text-muted-foreground text-xs">
|
||||||
{health ? 'Connected' : 'Disconnected'}
|
{health ? t('graphPanel.statusIndicator.connected') : t('graphPanel.statusIndicator.disconnected')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
@@ -3,12 +3,14 @@ import { useCallback } from 'react'
|
|||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
import { ZoomInIcon, ZoomOutIcon, FullscreenIcon } from 'lucide-react'
|
import { ZoomInIcon, ZoomOutIcon, FullscreenIcon } from 'lucide-react'
|
||||||
import { controlButtonVariant } from '@/lib/constants'
|
import { controlButtonVariant } from '@/lib/constants'
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that provides zoom controls for the graph viewer.
|
* Component that provides zoom controls for the graph viewer.
|
||||||
*/
|
*/
|
||||||
const ZoomControl = () => {
|
const ZoomControl = () => {
|
||||||
const { zoomIn, zoomOut, reset } = useCamera({ duration: 200, factor: 1.5 })
|
const { zoomIn, zoomOut, reset } = useCamera({ duration: 200, factor: 1.5 })
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleZoomIn = useCallback(() => zoomIn(), [zoomIn])
|
const handleZoomIn = useCallback(() => zoomIn(), [zoomIn])
|
||||||
const handleZoomOut = useCallback(() => zoomOut(), [zoomOut])
|
const handleZoomOut = useCallback(() => zoomOut(), [zoomOut])
|
||||||
@@ -16,16 +18,16 @@ const ZoomControl = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button variant={controlButtonVariant} onClick={handleZoomIn} tooltip="Zoom In" size="icon">
|
<Button variant={controlButtonVariant} onClick={handleZoomIn} tooltip={t("graphPanel.sideBar.zoomControl.zoomIn")} size="icon">
|
||||||
<ZoomInIcon />
|
<ZoomInIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant={controlButtonVariant} onClick={handleZoomOut} tooltip="Zoom Out" size="icon">
|
<Button variant={controlButtonVariant} onClick={handleZoomOut} tooltip={t("graphPanel.sideBar.zoomControl.zoomOut")} size="icon">
|
||||||
<ZoomOutIcon />
|
<ZoomOutIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={controlButtonVariant}
|
variant={controlButtonVariant}
|
||||||
onClick={handleResetZoom}
|
onClick={handleResetZoom}
|
||||||
tooltip="Reset Zoom"
|
tooltip={t("graphPanel.sideBar.zoomControl.resetZoom")}
|
||||||
size="icon"
|
size="icon"
|
||||||
>
|
>
|
||||||
<FullscreenIcon />
|
<FullscreenIcon />
|
||||||
|
@@ -15,18 +15,21 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
|||||||
import { oneLight, oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
|
import { oneLight, oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
|
||||||
|
|
||||||
import { LoaderIcon, CopyIcon } from 'lucide-react'
|
import { LoaderIcon, CopyIcon } from 'lucide-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export type MessageWithError = Message & {
|
export type MessageWithError = Message & {
|
||||||
isError?: boolean
|
isError?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatMessage = ({ message }: { message: MessageWithError }) => {
|
export const ChatMessage = ({ message }: { message: MessageWithError }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const handleCopyMarkdown = useCallback(async () => {
|
const handleCopyMarkdown = useCallback(async () => {
|
||||||
if (message.content) {
|
if (message.content) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(message.content)
|
await navigator.clipboard.writeText(message.content)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to copy:', err)
|
console.error(t('chat.copyError'), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [message])
|
}, [message])
|
||||||
@@ -57,7 +60,7 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => {
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleCopyMarkdown}
|
onClick={handleCopyMarkdown}
|
||||||
className="absolute right-0 bottom-0 size-6 rounded-md opacity-20 transition-opacity hover:opacity-100"
|
className="absolute right-0 bottom-0 size-6 rounded-md opacity-20 transition-opacity hover:opacity-100"
|
||||||
tooltip="Copy to clipboard"
|
tooltip={t('retrievePanel.chatMessage.copyTooltip')}
|
||||||
variant="default"
|
variant="default"
|
||||||
size="icon"
|
size="icon"
|
||||||
>
|
>
|
||||||
|
@@ -14,8 +14,10 @@ import {
|
|||||||
SelectValue
|
SelectValue
|
||||||
} from '@/components/ui/Select'
|
} from '@/components/ui/Select'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export default function QuerySettings() {
|
export default function QuerySettings() {
|
||||||
|
const { t } = useTranslation()
|
||||||
const querySettings = useSettingsStore((state) => state.querySettings)
|
const querySettings = useSettingsStore((state) => state.querySettings)
|
||||||
|
|
||||||
const handleChange = useCallback((key: keyof QueryRequest, value: any) => {
|
const handleChange = useCallback((key: keyof QueryRequest, value: any) => {
|
||||||
@@ -25,8 +27,8 @@ export default function QuerySettings() {
|
|||||||
return (
|
return (
|
||||||
<Card className="flex shrink-0 flex-col">
|
<Card className="flex shrink-0 flex-col">
|
||||||
<CardHeader className="px-4 pt-4 pb-2">
|
<CardHeader className="px-4 pt-4 pb-2">
|
||||||
<CardTitle>Parameters</CardTitle>
|
<CardTitle>{t('retrievePanel.querySettings.parametersTitle')}</CardTitle>
|
||||||
<CardDescription>Configure your query parameters</CardDescription>
|
<CardDescription>{t('retrievePanel.querySettings.parametersDescription')}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="m-0 flex grow flex-col p-0 text-xs">
|
<CardContent className="m-0 flex grow flex-col p-0 text-xs">
|
||||||
<div className="relative size-full">
|
<div className="relative size-full">
|
||||||
@@ -35,8 +37,8 @@ export default function QuerySettings() {
|
|||||||
<>
|
<>
|
||||||
<Text
|
<Text
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
text="Query Mode"
|
text={t('retrievePanel.querySettings.queryMode')}
|
||||||
tooltip="Select the retrieval strategy:\n• Naive: Basic search without advanced techniques\n• Local: Context-dependent information retrieval\n• Global: Utilizes global knowledge base\n• Hybrid: Combines local and global retrieval\n• Mix: Integrates knowledge graph with vector retrieval"
|
tooltip={t('retrievePanel.querySettings.queryModeTooltip')}
|
||||||
side="left"
|
side="left"
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
@@ -48,11 +50,11 @@ export default function QuerySettings() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectItem value="naive">Naive</SelectItem>
|
<SelectItem value="naive">{t('retrievePanel.querySettings.queryModeOptions.naive')}</SelectItem>
|
||||||
<SelectItem value="local">Local</SelectItem>
|
<SelectItem value="local">{t('retrievePanel.querySettings.queryModeOptions.local')}</SelectItem>
|
||||||
<SelectItem value="global">Global</SelectItem>
|
<SelectItem value="global">{t('retrievePanel.querySettings.queryModeOptions.global')}</SelectItem>
|
||||||
<SelectItem value="hybrid">Hybrid</SelectItem>
|
<SelectItem value="hybrid">{t('retrievePanel.querySettings.queryModeOptions.hybrid')}</SelectItem>
|
||||||
<SelectItem value="mix">Mix</SelectItem>
|
<SelectItem value="mix">{t('retrievePanel.querySettings.queryModeOptions.mix')}</SelectItem>
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -62,8 +64,8 @@ export default function QuerySettings() {
|
|||||||
<>
|
<>
|
||||||
<Text
|
<Text
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
text="Response Format"
|
text={t('retrievePanel.querySettings.responseFormat')}
|
||||||
tooltip="Defines the response format. Examples:\n• Multiple Paragraphs\n• Single Paragraph\n• Bullet Points"
|
tooltip={t('retrievePanel.querySettings.responseFormatTooltip')}
|
||||||
side="left"
|
side="left"
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
@@ -75,9 +77,9 @@ export default function QuerySettings() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectItem value="Multiple Paragraphs">Multiple Paragraphs</SelectItem>
|
<SelectItem value="Multiple Paragraphs">{t('retrievePanel.querySettings.responseFormatOptions.multipleParagraphs')}</SelectItem>
|
||||||
<SelectItem value="Single Paragraph">Single Paragraph</SelectItem>
|
<SelectItem value="Single Paragraph">{t('retrievePanel.querySettings.responseFormatOptions.singleParagraph')}</SelectItem>
|
||||||
<SelectItem value="Bullet Points">Bullet Points</SelectItem>
|
<SelectItem value="Bullet Points">{t('retrievePanel.querySettings.responseFormatOptions.bulletPoints')}</SelectItem>
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -87,8 +89,8 @@ export default function QuerySettings() {
|
|||||||
<>
|
<>
|
||||||
<Text
|
<Text
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
text="Top K Results"
|
text={t('retrievePanel.querySettings.topK')}
|
||||||
tooltip="Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode"
|
tooltip={t('retrievePanel.querySettings.topKTooltip')}
|
||||||
side="left"
|
side="left"
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
@@ -97,7 +99,7 @@ export default function QuerySettings() {
|
|||||||
value={querySettings.top_k}
|
value={querySettings.top_k}
|
||||||
onValueChange={(v) => handleChange('top_k', v)}
|
onValueChange={(v) => handleChange('top_k', v)}
|
||||||
min={1}
|
min={1}
|
||||||
placeholder="Number of results"
|
placeholder={t('retrievePanel.querySettings.topKPlaceholder')}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
@@ -106,8 +108,8 @@ export default function QuerySettings() {
|
|||||||
<>
|
<>
|
||||||
<Text
|
<Text
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
text="Max Tokens for Text Unit"
|
text={t('retrievePanel.querySettings.maxTokensTextUnit')}
|
||||||
tooltip="Maximum number of tokens allowed for each retrieved text chunk"
|
tooltip={t('retrievePanel.querySettings.maxTokensTextUnitTooltip')}
|
||||||
side="left"
|
side="left"
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
@@ -116,14 +118,14 @@ export default function QuerySettings() {
|
|||||||
value={querySettings.max_token_for_text_unit}
|
value={querySettings.max_token_for_text_unit}
|
||||||
onValueChange={(v) => handleChange('max_token_for_text_unit', v)}
|
onValueChange={(v) => handleChange('max_token_for_text_unit', v)}
|
||||||
min={1}
|
min={1}
|
||||||
placeholder="Max tokens for text unit"
|
placeholder={t('retrievePanel.querySettings.maxTokensTextUnit')}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<Text
|
<Text
|
||||||
text="Max Tokens for Global Context"
|
text={t('retrievePanel.querySettings.maxTokensGlobalContext')}
|
||||||
tooltip="Maximum number of tokens allocated for relationship descriptions in global retrieval"
|
tooltip={t('retrievePanel.querySettings.maxTokensGlobalContextTooltip')}
|
||||||
side="left"
|
side="left"
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
@@ -132,15 +134,15 @@ export default function QuerySettings() {
|
|||||||
value={querySettings.max_token_for_global_context}
|
value={querySettings.max_token_for_global_context}
|
||||||
onValueChange={(v) => handleChange('max_token_for_global_context', v)}
|
onValueChange={(v) => handleChange('max_token_for_global_context', v)}
|
||||||
min={1}
|
min={1}
|
||||||
placeholder="Max tokens for global context"
|
placeholder={t('retrievePanel.querySettings.maxTokensGlobalContext')}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<Text
|
<Text
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
text="Max Tokens for Local Context"
|
text={t('retrievePanel.querySettings.maxTokensLocalContext')}
|
||||||
tooltip="Maximum number of tokens allocated for entity descriptions in local retrieval"
|
tooltip={t('retrievePanel.querySettings.maxTokensLocalContextTooltip')}
|
||||||
side="left"
|
side="left"
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
@@ -149,7 +151,7 @@ export default function QuerySettings() {
|
|||||||
value={querySettings.max_token_for_local_context}
|
value={querySettings.max_token_for_local_context}
|
||||||
onValueChange={(v) => handleChange('max_token_for_local_context', v)}
|
onValueChange={(v) => handleChange('max_token_for_local_context', v)}
|
||||||
min={1}
|
min={1}
|
||||||
placeholder="Max tokens for local context"
|
placeholder={t('retrievePanel.querySettings.maxTokensLocalContext')}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
</>
|
</>
|
||||||
@@ -158,8 +160,8 @@ export default function QuerySettings() {
|
|||||||
<>
|
<>
|
||||||
<Text
|
<Text
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
text="History Turns"
|
text={t('retrievePanel.querySettings.historyTurns')}
|
||||||
tooltip="Number of complete conversation turns (user-assistant pairs) to consider in the response context"
|
tooltip={t('retrievePanel.querySettings.historyTurnsTooltip')}
|
||||||
side="left"
|
side="left"
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
@@ -170,7 +172,7 @@ export default function QuerySettings() {
|
|||||||
value={querySettings.history_turns}
|
value={querySettings.history_turns}
|
||||||
onValueChange={(v) => handleChange('history_turns', v)}
|
onValueChange={(v) => handleChange('history_turns', v)}
|
||||||
min={0}
|
min={0}
|
||||||
placeholder="Number of history turns"
|
placeholder={t('retrievePanel.querySettings.historyTurnsPlaceholder')}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
@@ -179,8 +181,8 @@ export default function QuerySettings() {
|
|||||||
<>
|
<>
|
||||||
<Text
|
<Text
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
text="High-Level Keywords"
|
text={t('retrievePanel.querySettings.hlKeywords')}
|
||||||
tooltip="List of high-level keywords to prioritize in retrieval. Separate with commas"
|
tooltip={t('retrievePanel.querySettings.hlKeywordsTooltip')}
|
||||||
side="left"
|
side="left"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
@@ -194,15 +196,15 @@ export default function QuerySettings() {
|
|||||||
.filter((k) => k !== '')
|
.filter((k) => k !== '')
|
||||||
handleChange('hl_keywords', keywords)
|
handleChange('hl_keywords', keywords)
|
||||||
}}
|
}}
|
||||||
placeholder="Enter keywords"
|
placeholder={t('retrievePanel.querySettings.hlkeywordsPlaceHolder')}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<Text
|
<Text
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
text="Low-Level Keywords"
|
text={t('retrievePanel.querySettings.llKeywords')}
|
||||||
tooltip="List of low-level keywords to refine retrieval focus. Separate with commas"
|
tooltip={t('retrievePanel.querySettings.llKeywordsTooltip')}
|
||||||
side="left"
|
side="left"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
@@ -216,7 +218,7 @@ export default function QuerySettings() {
|
|||||||
.filter((k) => k !== '')
|
.filter((k) => k !== '')
|
||||||
handleChange('ll_keywords', keywords)
|
handleChange('ll_keywords', keywords)
|
||||||
}}
|
}}
|
||||||
placeholder="Enter keywords"
|
placeholder={t('retrievePanel.querySettings.hlkeywordsPlaceHolder')}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
</>
|
</>
|
||||||
@@ -226,8 +228,8 @@ export default function QuerySettings() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Text
|
<Text
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
text="Only Need Context"
|
text={t('retrievePanel.querySettings.onlyNeedContext')}
|
||||||
tooltip="If True, only returns the retrieved context without generating a response"
|
tooltip={t('retrievePanel.querySettings.onlyNeedContextTooltip')}
|
||||||
side="left"
|
side="left"
|
||||||
/>
|
/>
|
||||||
<div className="grow" />
|
<div className="grow" />
|
||||||
@@ -242,8 +244,8 @@ export default function QuerySettings() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Text
|
<Text
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
text="Only Need Prompt"
|
text={t('retrievePanel.querySettings.onlyNeedPrompt')}
|
||||||
tooltip="If True, only returns the generated prompt without producing a response"
|
tooltip={t('retrievePanel.querySettings.onlyNeedPromptTooltip')}
|
||||||
side="left"
|
side="left"
|
||||||
/>
|
/>
|
||||||
<div className="grow" />
|
<div className="grow" />
|
||||||
@@ -258,8 +260,8 @@ export default function QuerySettings() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Text
|
<Text
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
text="Stream Response"
|
text={t('retrievePanel.querySettings.streamResponse')}
|
||||||
tooltip="If True, enables streaming output for real-time responses"
|
tooltip={t('retrievePanel.querySettings.streamResponseTooltip')}
|
||||||
side="left"
|
side="left"
|
||||||
/>
|
/>
|
||||||
<div className="grow" />
|
<div className="grow" />
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -22,6 +23,7 @@ import { useBackendState } from '@/stores/state'
|
|||||||
import { RefreshCwIcon } from 'lucide-react'
|
import { RefreshCwIcon } from 'lucide-react'
|
||||||
|
|
||||||
export default function DocumentManager() {
|
export default function DocumentManager() {
|
||||||
|
const { t } = useTranslation()
|
||||||
const health = useBackendState.use.health()
|
const health = useBackendState.use.health()
|
||||||
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
|
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
|
||||||
|
|
||||||
@@ -44,7 +46,7 @@ export default function DocumentManager() {
|
|||||||
setDocs(null)
|
setDocs(null)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('Failed to load documents\n' + errorMessage(err))
|
toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }))
|
||||||
}
|
}
|
||||||
}, [setDocs])
|
}, [setDocs])
|
||||||
|
|
||||||
@@ -57,7 +59,7 @@ export default function DocumentManager() {
|
|||||||
const { status } = await scanNewDocuments()
|
const { status } = await scanNewDocuments()
|
||||||
toast.message(status)
|
toast.message(status)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('Failed to load documents\n' + errorMessage(err))
|
toast.error(t('documentPanel.documentManager.errors.scanFailed', { error: errorMessage(err) }))
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -69,7 +71,7 @@ export default function DocumentManager() {
|
|||||||
try {
|
try {
|
||||||
await fetchDocuments()
|
await fetchDocuments()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('Failed to get scan progress\n' + errorMessage(err))
|
toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }))
|
||||||
}
|
}
|
||||||
}, 5000)
|
}, 5000)
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
@@ -78,7 +80,7 @@ export default function DocumentManager() {
|
|||||||
return (
|
return (
|
||||||
<Card className="!size-full !rounded-none !border-none">
|
<Card className="!size-full !rounded-none !border-none">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg">Document Management</CardTitle>
|
<CardTitle className="text-lg">{t('documentPanel.documentManager.title')}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -86,10 +88,10 @@ export default function DocumentManager() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={scanDocuments}
|
onClick={scanDocuments}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
tooltip="Scan documents"
|
tooltip={t('documentPanel.documentManager.scanTooltip')}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<RefreshCwIcon /> Scan
|
<RefreshCwIcon /> {t('documentPanel.documentManager.scanButton')}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
<ClearDocumentsDialog />
|
<ClearDocumentsDialog />
|
||||||
@@ -98,29 +100,29 @@ export default function DocumentManager() {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Uploaded documents</CardTitle>
|
<CardTitle>{t('documentPanel.documentManager.uploadedTitle')}</CardTitle>
|
||||||
<CardDescription>view the uploaded documents here</CardDescription>
|
<CardDescription>{t('documentPanel.documentManager.uploadedDescription')}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{!docs && (
|
{!docs && (
|
||||||
<EmptyCard
|
<EmptyCard
|
||||||
title="No documents uploaded"
|
title={t('documentPanel.documentManager.emptyTitle')}
|
||||||
description="upload documents to see them here"
|
description={t('documentPanel.documentManager.emptyDescription')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{docs && (
|
{docs && (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>ID</TableHead>
|
<TableHead>{t('documentPanel.documentManager.columns.id')}</TableHead>
|
||||||
<TableHead>Summary</TableHead>
|
<TableHead>{t('documentPanel.documentManager.columns.summary')}</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>{t('documentPanel.documentManager.columns.status')}</TableHead>
|
||||||
<TableHead>Length</TableHead>
|
<TableHead>{t('documentPanel.documentManager.columns.length')}</TableHead>
|
||||||
<TableHead>Chunks</TableHead>
|
<TableHead>{t('documentPanel.documentManager.columns.chunks')}</TableHead>
|
||||||
<TableHead>Created</TableHead>
|
<TableHead>{t('documentPanel.documentManager.columns.created')}</TableHead>
|
||||||
<TableHead>Updated</TableHead>
|
<TableHead>{t('documentPanel.documentManager.columns.updated')}</TableHead>
|
||||||
<TableHead>Metadata</TableHead>
|
<TableHead>{t('documentPanel.documentManager.columns.metadata')}</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody className="text-sm">
|
<TableBody className="text-sm">
|
||||||
@@ -137,13 +139,13 @@ export default function DocumentManager() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{status === 'processed' && (
|
{status === 'processed' && (
|
||||||
<span className="text-green-600">Completed</span>
|
<span className="text-green-600">{t('documentPanel.documentManager.status.completed')}</span>
|
||||||
)}
|
)}
|
||||||
{status === 'processing' && (
|
{status === 'processing' && (
|
||||||
<span className="text-blue-600">Processing</span>
|
<span className="text-blue-600">{t('documentPanel.documentManager.status.processing')}</span>
|
||||||
)}
|
)}
|
||||||
{status === 'pending' && <span className="text-yellow-600">Pending</span>}
|
{status === 'pending' && <span className="text-yellow-600">{t('documentPanel.documentManager.status.pending')}</span>}
|
||||||
{status === 'failed' && <span className="text-red-600">Failed</span>}
|
{status === 'failed' && <span className="text-red-600">{t('documentPanel.documentManager.status.failed')}</span>}
|
||||||
{doc.error && (
|
{doc.error && (
|
||||||
<span className="ml-2 text-red-500" title={doc.error}>
|
<span className="ml-2 text-red-500" title={doc.error}>
|
||||||
⚠️
|
⚠️
|
||||||
|
@@ -8,8 +8,10 @@ import { useDebounce } from '@/hooks/useDebounce'
|
|||||||
import QuerySettings from '@/components/retrieval/QuerySettings'
|
import QuerySettings from '@/components/retrieval/QuerySettings'
|
||||||
import { ChatMessage, MessageWithError } from '@/components/retrieval/ChatMessage'
|
import { ChatMessage, MessageWithError } from '@/components/retrieval/ChatMessage'
|
||||||
import { EraserIcon, SendIcon } from 'lucide-react'
|
import { EraserIcon, SendIcon } from 'lucide-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export default function RetrievalTesting() {
|
export default function RetrievalTesting() {
|
||||||
|
const { t } = useTranslation()
|
||||||
const [messages, setMessages] = useState<MessageWithError[]>(
|
const [messages, setMessages] = useState<MessageWithError[]>(
|
||||||
() => useSettingsStore.getState().retrievalHistory || []
|
() => useSettingsStore.getState().retrievalHistory || []
|
||||||
)
|
)
|
||||||
@@ -89,7 +91,7 @@ export default function RetrievalTesting() {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Handle error
|
// Handle error
|
||||||
updateAssistantMessage(`Error: Failed to get response\n${errorMessage(err)}`, true)
|
updateAssistantMessage(`${t('retrievePanel.retrieval.error')}\n${errorMessage(err)}`, true)
|
||||||
} finally {
|
} finally {
|
||||||
// Clear loading and add messages to state
|
// Clear loading and add messages to state
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
@@ -98,7 +100,7 @@ export default function RetrievalTesting() {
|
|||||||
.setRetrievalHistory([...prevMessages, userMessage, assistantMessage])
|
.setRetrievalHistory([...prevMessages, userMessage, assistantMessage])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[inputValue, isLoading, messages, setMessages]
|
[inputValue, isLoading, messages, setMessages, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
const debouncedMessages = useDebounce(messages, 100)
|
const debouncedMessages = useDebounce(messages, 100)
|
||||||
@@ -117,7 +119,7 @@ export default function RetrievalTesting() {
|
|||||||
<div className="flex min-h-0 flex-1 flex-col gap-2">
|
<div className="flex min-h-0 flex-1 flex-col gap-2">
|
||||||
{messages.length === 0 ? (
|
{messages.length === 0 ? (
|
||||||
<div className="text-muted-foreground flex h-full items-center justify-center text-lg">
|
<div className="text-muted-foreground flex h-full items-center justify-center text-lg">
|
||||||
Start a retrieval by typing your query below
|
{t('retrievePanel.retrieval.startPrompt')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
messages.map((message, idx) => (
|
messages.map((message, idx) => (
|
||||||
@@ -143,18 +145,18 @@ export default function RetrievalTesting() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<EraserIcon />
|
<EraserIcon />
|
||||||
Clear
|
{t('retrievePanel.retrieval.clear')}
|
||||||
</Button>
|
</Button>
|
||||||
<Input
|
<Input
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
placeholder="Type your query..."
|
placeholder={t('retrievePanel.retrieval.placeholder')}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" variant="default" disabled={isLoading} size="sm">
|
<Button type="submit" variant="default" disabled={isLoading} size="sm">
|
||||||
<SendIcon />
|
<SendIcon />
|
||||||
Send
|
{t('retrievePanel.retrieval.send')}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -4,6 +4,7 @@ import ThemeToggle from '@/components/ThemeToggle'
|
|||||||
import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
|
import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { ZapIcon, GithubIcon } from 'lucide-react'
|
import { ZapIcon, GithubIcon } from 'lucide-react'
|
||||||
|
|
||||||
@@ -29,21 +30,22 @@ function NavigationTab({ value, currentTab, children }: NavigationTabProps) {
|
|||||||
|
|
||||||
function TabsNavigation() {
|
function TabsNavigation() {
|
||||||
const currentTab = useSettingsStore.use.currentTab()
|
const currentTab = useSettingsStore.use.currentTab()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-8 self-center">
|
<div className="flex h-8 self-center">
|
||||||
<TabsList className="h-full gap-2">
|
<TabsList className="h-full gap-2">
|
||||||
<NavigationTab value="documents" currentTab={currentTab}>
|
<NavigationTab value="documents" currentTab={currentTab}>
|
||||||
Documents
|
{t('header.documents')}
|
||||||
</NavigationTab>
|
</NavigationTab>
|
||||||
<NavigationTab value="knowledge-graph" currentTab={currentTab}>
|
<NavigationTab value="knowledge-graph" currentTab={currentTab}>
|
||||||
Knowledge Graph
|
{t('header.knowledgeGraph')}
|
||||||
</NavigationTab>
|
</NavigationTab>
|
||||||
<NavigationTab value="retrieval" currentTab={currentTab}>
|
<NavigationTab value="retrieval" currentTab={currentTab}>
|
||||||
Retrieval
|
{t('header.retrieval')}
|
||||||
</NavigationTab>
|
</NavigationTab>
|
||||||
<NavigationTab value="api" currentTab={currentTab}>
|
<NavigationTab value="api" currentTab={currentTab}>
|
||||||
API
|
{t('header.api')}
|
||||||
</NavigationTab>
|
</NavigationTab>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,6 +53,7 @@ function TabsNavigation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SiteHeader() {
|
export default function SiteHeader() {
|
||||||
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<header className="border-border/40 bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 flex h-10 w-full border-b px-4 backdrop-blur">
|
<header className="border-border/40 bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 flex h-10 w-full border-b px-4 backdrop-blur">
|
||||||
<a href="/" className="mr-6 flex items-center gap-2">
|
<a href="/" className="mr-6 flex items-center gap-2">
|
||||||
@@ -64,7 +67,7 @@ export default function SiteHeader() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex items-center">
|
<nav className="flex items-center">
|
||||||
<Button variant="ghost" size="icon" side="bottom" tooltip="Project Repository">
|
<Button variant="ghost" size="icon" side="bottom" tooltip={t('header.projectRepository')}>
|
||||||
<a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
|
<a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
|
||||||
<GithubIcon className="size-4" aria-hidden="true" />
|
<GithubIcon className="size-4" aria-hidden="true" />
|
||||||
</a>
|
</a>
|
||||||
|
21
lightrag_webui/src/i18n.js
Normal file
21
lightrag_webui/src/i18n.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import i18n from "i18next";
|
||||||
|
import { initReactI18next } from "react-i18next";
|
||||||
|
|
||||||
|
import en from "./locales/en.json";
|
||||||
|
import zh from "./locales/zh.json";
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
resources: {
|
||||||
|
en: { translation: en },
|
||||||
|
zh: { translation: zh }
|
||||||
|
},
|
||||||
|
lng: "en", // default
|
||||||
|
fallbackLng: "en",
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
234
lightrag_webui/src/locales/en.json
Normal file
234
lightrag_webui/src/locales/en.json
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"documents": "Documents",
|
||||||
|
"knowledgeGraph": "Knowledge Graph",
|
||||||
|
"retrieval": "Retrieval",
|
||||||
|
"api": "API",
|
||||||
|
"projectRepository": "Project Repository",
|
||||||
|
"themeToggle": {
|
||||||
|
"switchToLight": "Switch to light theme",
|
||||||
|
"switchToDark": "Switch to dark theme"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"documentPanel": {
|
||||||
|
"clearDocuments": {
|
||||||
|
"button": "Clear",
|
||||||
|
"tooltip": "Clear documents",
|
||||||
|
"title": "Clear Documents",
|
||||||
|
"confirm": "Do you really want to clear all documents?",
|
||||||
|
"confirmButton": "YES",
|
||||||
|
"success": "Documents cleared successfully",
|
||||||
|
"failed": "Clear Documents Failed:\n{{message}}",
|
||||||
|
"error": "Clear Documents Failed:\n{{error}}"
|
||||||
|
},
|
||||||
|
"uploadDocuments": {
|
||||||
|
"button": "Upload",
|
||||||
|
"tooltip": "Upload documents",
|
||||||
|
"title": "Upload Documents",
|
||||||
|
"description": "Drag and drop your documents here or click to browse.",
|
||||||
|
"uploading": "Uploading {{name}}: {{percent}}%",
|
||||||
|
"success": "Upload Success:\n{{name}} uploaded successfully",
|
||||||
|
"failed": "Upload Failed:\n{{name}}\n{{message}}",
|
||||||
|
"error": "Upload Failed:\n{{name}}\n{{error}}",
|
||||||
|
"generalError": "Upload Failed\n{{error}}",
|
||||||
|
"fileTypes": "Supported types: TXT, MD, DOCX, PDF, PPTX, RTF, ODT, EPUB, HTML, HTM, TEX, JSON, XML, YAML, YML, CSV, LOG, CONF, INI, PROPERTIES, SQL, BAT, SH, C, CPP, PY, JAVA, JS, TS, SWIFT, GO, RB, PHP, CSS, SCSS, LESS"
|
||||||
|
},
|
||||||
|
"documentManager": {
|
||||||
|
"title": "Document Management",
|
||||||
|
"scanButton": "Scan",
|
||||||
|
"scanTooltip": "Scan documents",
|
||||||
|
"uploadedTitle": "Uploaded Documents",
|
||||||
|
"uploadedDescription": "List of uploaded documents and their statuses.",
|
||||||
|
"emptyTitle": "No Documents",
|
||||||
|
"emptyDescription": "There are no uploaded documents yet.",
|
||||||
|
"columns": {
|
||||||
|
"id": "ID",
|
||||||
|
"summary": "Summary",
|
||||||
|
"status": "Status",
|
||||||
|
"length": "Length",
|
||||||
|
"chunks": "Chunks",
|
||||||
|
"created": "Created",
|
||||||
|
"updated": "Updated",
|
||||||
|
"metadata": "Metadata"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"completed": "Completed",
|
||||||
|
"processing": "Processing",
|
||||||
|
"pending": "Pending",
|
||||||
|
"failed": "Failed"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"loadFailed": "Failed to load documents\n{{error}}",
|
||||||
|
"scanFailed": "Failed to scan documents\n{{error}}",
|
||||||
|
"scanProgressFailed": "Failed to get scan progress\n{{error}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graphPanel": {
|
||||||
|
"sideBar": {
|
||||||
|
"settings": {
|
||||||
|
"settings": "Settings",
|
||||||
|
"healthCheck": "Health Check",
|
||||||
|
"showPropertyPanel": "Show Property Panel",
|
||||||
|
"showSearchBar": "Show Search Bar",
|
||||||
|
"showNodeLabel": "Show Node Label",
|
||||||
|
"nodeDraggable": "Node Draggable",
|
||||||
|
"showEdgeLabel": "Show Edge Label",
|
||||||
|
"hideUnselectedEdges": "Hide Unselected Edges",
|
||||||
|
"edgeEvents": "Edge Events",
|
||||||
|
"maxQueryDepth": "Max Query Depth",
|
||||||
|
"minDegree": "Minimum Degree",
|
||||||
|
"maxLayoutIterations": "Max Layout Iterations",
|
||||||
|
"apiKey": "API Key",
|
||||||
|
"enterYourAPIkey": "Enter your API key",
|
||||||
|
"save": "Save"
|
||||||
|
},
|
||||||
|
|
||||||
|
"zoomControl": {
|
||||||
|
"zoomIn": "Zoom In",
|
||||||
|
"zoomOut": "Zoom Out",
|
||||||
|
"resetZoom": "Reset Zoom"
|
||||||
|
},
|
||||||
|
|
||||||
|
"layoutsControl": {
|
||||||
|
"startAnimation": "Start the layout animation",
|
||||||
|
"stopAnimation": "Stop the layout animation",
|
||||||
|
"layoutGraph": "Layout Graph",
|
||||||
|
"layouts": {
|
||||||
|
"Circular": "Circular",
|
||||||
|
"Circlepack": "Circlepack",
|
||||||
|
"Random": "Random",
|
||||||
|
"Noverlaps": "Noverlaps",
|
||||||
|
"Force Directed": "Force Directed",
|
||||||
|
"Force Atlas": "Force Atlas"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"fullScreenControl": {
|
||||||
|
"fullScreen": "Full Screen",
|
||||||
|
"windowed": "Windowed"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"statusIndicator": {
|
||||||
|
"connected": "Connected",
|
||||||
|
"disconnected": "Disconnected"
|
||||||
|
},
|
||||||
|
"statusCard": {
|
||||||
|
"unavailable": "Status information unavailable",
|
||||||
|
"storageInfo": "Storage Info",
|
||||||
|
"workingDirectory": "Working Directory",
|
||||||
|
"inputDirectory": "Input Directory",
|
||||||
|
"llmConfig": "LLM Configuration",
|
||||||
|
"llmBinding": "LLM Binding",
|
||||||
|
"llmBindingHost": "LLM Binding Host",
|
||||||
|
"llmModel": "LLM Model",
|
||||||
|
"maxTokens": "Max Tokens",
|
||||||
|
"embeddingConfig": "Embedding Configuration",
|
||||||
|
"embeddingBinding": "Embedding Binding",
|
||||||
|
"embeddingBindingHost": "Embedding Binding Host",
|
||||||
|
"embeddingModel": "Embedding Model",
|
||||||
|
"storageConfig": "Storage Configuration",
|
||||||
|
"kvStorage": "KV Storage",
|
||||||
|
"docStatusStorage": "Doc Status Storage",
|
||||||
|
"graphStorage": "Graph Storage",
|
||||||
|
"vectorStorage": "Vector Storage"
|
||||||
|
},
|
||||||
|
"propertiesView": {
|
||||||
|
"node": {
|
||||||
|
"title": "Node",
|
||||||
|
"id": "ID",
|
||||||
|
"labels": "Labels",
|
||||||
|
"degree": "Degree",
|
||||||
|
"properties": "Properties",
|
||||||
|
"relationships": "Relationships"
|
||||||
|
},
|
||||||
|
"edge": {
|
||||||
|
"title": "Relationship",
|
||||||
|
"id": "ID",
|
||||||
|
"type": "Type",
|
||||||
|
"source": "Source",
|
||||||
|
"target": "Target",
|
||||||
|
"properties": "Properties"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Search nodes...",
|
||||||
|
"message": "And {count} others"
|
||||||
|
},
|
||||||
|
"graphLabels": {
|
||||||
|
"selectTooltip": "Select query label",
|
||||||
|
"noLabels": "No labels found",
|
||||||
|
"label": "Label",
|
||||||
|
"placeholder": "Search labels...",
|
||||||
|
"andOthers": "And {count} others"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"retrievePanel": {
|
||||||
|
"chatMessage": {
|
||||||
|
"copyTooltip": "Copy to clipboard",
|
||||||
|
"copyError": "Failed to copy text to clipboard"
|
||||||
|
},
|
||||||
|
"retrieval": {
|
||||||
|
"startPrompt": "Start a retrieval by typing your query below",
|
||||||
|
"clear": "Clear",
|
||||||
|
"send": "Send",
|
||||||
|
"placeholder": "Type your query...",
|
||||||
|
"error": "Error: Failed to get response"
|
||||||
|
},
|
||||||
|
"querySettings": {
|
||||||
|
"parametersTitle": "Parameters",
|
||||||
|
"parametersDescription": "Configure your query parameters",
|
||||||
|
|
||||||
|
"queryMode": "Query Mode",
|
||||||
|
"queryModeTooltip": "Select the retrieval strategy:\n• Naive: Basic search without advanced techniques\n• Local: Context-dependent information retrieval\n• Global: Utilizes global knowledge base\n• Hybrid: Combines local and global retrieval\n• Mix: Integrates knowledge graph with vector retrieval",
|
||||||
|
"queryModeOptions": {
|
||||||
|
"naive": "Naive",
|
||||||
|
"local": "Local",
|
||||||
|
"global": "Global",
|
||||||
|
"hybrid": "Hybrid",
|
||||||
|
"mix": "Mix"
|
||||||
|
},
|
||||||
|
|
||||||
|
"responseFormat": "Response Format",
|
||||||
|
"responseFormatTooltip": "Defines the response format. Examples:\n• Multiple Paragraphs\n• Single Paragraph\n• Bullet Points",
|
||||||
|
"responseFormatOptions": {
|
||||||
|
"multipleParagraphs": "Multiple Paragraphs",
|
||||||
|
"singleParagraph": "Single Paragraph",
|
||||||
|
"bulletPoints": "Bullet Points"
|
||||||
|
},
|
||||||
|
|
||||||
|
"topK": "Top K Results",
|
||||||
|
"topKTooltip": "Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode",
|
||||||
|
"topKPlaceholder": "Number of results",
|
||||||
|
|
||||||
|
"maxTokensTextUnit": "Max Tokens for Text Unit",
|
||||||
|
"maxTokensTextUnitTooltip": "Maximum number of tokens allowed for each retrieved text chunk",
|
||||||
|
|
||||||
|
"maxTokensGlobalContext": "Max Tokens for Global Context",
|
||||||
|
"maxTokensGlobalContextTooltip": "Maximum number of tokens allocated for relationship descriptions in global retrieval",
|
||||||
|
|
||||||
|
"maxTokensLocalContext": "Max Tokens for Local Context",
|
||||||
|
"maxTokensLocalContextTooltip": "Maximum number of tokens allocated for entity descriptions in local retrieval",
|
||||||
|
|
||||||
|
"historyTurns": "History Turns",
|
||||||
|
"historyTurnsTooltip": "Number of complete conversation turns (user-assistant pairs) to consider in the response context",
|
||||||
|
"historyTurnsPlaceholder": "Number of history turns",
|
||||||
|
|
||||||
|
"hlKeywords": "High-Level Keywords",
|
||||||
|
"hlKeywordsTooltip": "List of high-level keywords to prioritize in retrieval. Separate with commas",
|
||||||
|
"hlkeywordsPlaceHolder": "Enter keywords",
|
||||||
|
|
||||||
|
"llKeywords": "Low-Level Keywords",
|
||||||
|
"llKeywordsTooltip": "List of low-level keywords to refine retrieval focus. Separate with commas",
|
||||||
|
|
||||||
|
"onlyNeedContext": "Only Need Context",
|
||||||
|
"onlyNeedContextTooltip": "If True, only returns the retrieved context without generating a response",
|
||||||
|
|
||||||
|
"onlyNeedPrompt": "Only Need Prompt",
|
||||||
|
"onlyNeedPromptTooltip": "If True, only returns the generated prompt without producing a response",
|
||||||
|
|
||||||
|
"streamResponse": "Stream Response",
|
||||||
|
"streamResponseTooltip": "If True, enables streaming output for real-time responses"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
235
lightrag_webui/src/locales/zh.json
Normal file
235
lightrag_webui/src/locales/zh.json
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"documents": "文档",
|
||||||
|
"knowledgeGraph": "知识图谱",
|
||||||
|
"retrieval": "检索",
|
||||||
|
"api": "API",
|
||||||
|
"projectRepository": "项目仓库",
|
||||||
|
"themeToggle": {
|
||||||
|
"switchToLight": "切换到亮色主题",
|
||||||
|
"switchToDark": "切换到暗色主题"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"documentPanel": {
|
||||||
|
"clearDocuments": {
|
||||||
|
"button": "清除",
|
||||||
|
"tooltip": "清除文档",
|
||||||
|
"title": "清除文档",
|
||||||
|
"confirm": "您确定要清除所有文档吗?",
|
||||||
|
"confirmButton": "确定",
|
||||||
|
"success": "文档已成功清除",
|
||||||
|
"failed": "清除文档失败:\n{{message}}",
|
||||||
|
"error": "清除文档失败:\n{{error}}"
|
||||||
|
},
|
||||||
|
"uploadDocuments": {
|
||||||
|
"button": "上传",
|
||||||
|
"tooltip": "上传文档",
|
||||||
|
"title": "上传文档",
|
||||||
|
"description": "拖放文档到此处或点击浏览。",
|
||||||
|
"uploading": "正在上传 {{name}}: {{percent}}%",
|
||||||
|
"success": "上传成功:\n{{name}} 上传成功",
|
||||||
|
"failed": "上传失败:\n{{name}}\n{{message}}",
|
||||||
|
"error": "上传失败:\n{{name}}\n{{error}}",
|
||||||
|
"generalError": "上传失败\n{{error}}",
|
||||||
|
"fileTypes": "支持的文件类型: TXT, MD, DOCX, PDF, PPTX, RTF, ODT, EPUB, HTML, HTM, TEX, JSON, XML, YAML, YML, CSV, LOG, CONF, INI, PROPERTIES, SQL, BAT, SH, C, CPP, PY, JAVA, JS, TS, SWIFT, GO, RB, PHP, CSS, SCSS, LESS"
|
||||||
|
},
|
||||||
|
"documentManager": {
|
||||||
|
"title": "文档管理",
|
||||||
|
"scanButton": "扫描",
|
||||||
|
"scanTooltip": "扫描文档",
|
||||||
|
"uploadedTitle": "已上传文档",
|
||||||
|
"uploadedDescription": "已上传文档及其状态列表。",
|
||||||
|
"emptyTitle": "暂无文档",
|
||||||
|
"emptyDescription": "尚未上传任何文档。",
|
||||||
|
"columns": {
|
||||||
|
"id": "ID",
|
||||||
|
"summary": "摘要",
|
||||||
|
"status": "状态",
|
||||||
|
"length": "长度",
|
||||||
|
"chunks": "分块",
|
||||||
|
"created": "创建时间",
|
||||||
|
"updated": "更新时间",
|
||||||
|
"metadata": "元数据"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"completed": "已完成",
|
||||||
|
"processing": "处理中",
|
||||||
|
"pending": "待处理",
|
||||||
|
"failed": "失败"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"loadFailed": "加载文档失败\n{{error}}",
|
||||||
|
"scanFailed": "扫描文档失败\n{{error}}",
|
||||||
|
"scanProgressFailed": "获取扫描进度失败\n{{error}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graphPanel": {
|
||||||
|
"sideBar": {
|
||||||
|
"settings": {
|
||||||
|
"settings": "设置",
|
||||||
|
"healthCheck": "健康检查",
|
||||||
|
"showPropertyPanel": "显示属性面板",
|
||||||
|
"showSearchBar": "显示搜索栏",
|
||||||
|
"showNodeLabel": "显示节点标签",
|
||||||
|
"nodeDraggable": "节点可拖动",
|
||||||
|
"showEdgeLabel": "显示边标签",
|
||||||
|
"hideUnselectedEdges": "隐藏未选中边",
|
||||||
|
"edgeEvents": "边事件",
|
||||||
|
"maxQueryDepth": "最大查询深度",
|
||||||
|
"minDegree": "最小度数",
|
||||||
|
"maxLayoutIterations": "最大布局迭代次数",
|
||||||
|
"apiKey": "API 密钥",
|
||||||
|
"enterYourAPIkey": "输入您的 API 密钥",
|
||||||
|
"save": "保存"
|
||||||
|
},
|
||||||
|
|
||||||
|
"zoomControl": {
|
||||||
|
"zoomIn": "放大",
|
||||||
|
"zoomOut": "缩小",
|
||||||
|
"resetZoom": "重置缩放"
|
||||||
|
},
|
||||||
|
|
||||||
|
"layoutsControl": {
|
||||||
|
"startAnimation": "开始布局动画",
|
||||||
|
"stopAnimation": "停止布局动画",
|
||||||
|
"layoutGraph": "布局图",
|
||||||
|
"layouts": {
|
||||||
|
"Circular": "环形布局",
|
||||||
|
"Circlepack": "圆形打包布局",
|
||||||
|
"Random": "随机布局",
|
||||||
|
"Noverlaps": "无重叠布局",
|
||||||
|
"Force Directed": "力导向布局",
|
||||||
|
"Force Atlas": "力导向图谱布局"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"fullScreenControl": {
|
||||||
|
"fullScreen": "全屏",
|
||||||
|
"windowed": "窗口模式"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"statusIndicator": {
|
||||||
|
"connected": "已连接",
|
||||||
|
"disconnected": "未连接"
|
||||||
|
},
|
||||||
|
"statusCard": {
|
||||||
|
"unavailable": "状态信息不可用",
|
||||||
|
"storageInfo": "存储信息",
|
||||||
|
"workingDirectory": "工作目录",
|
||||||
|
"inputDirectory": "输入目录",
|
||||||
|
"llmConfig": "LLM 配置",
|
||||||
|
"llmBinding": "LLM 绑定",
|
||||||
|
"llmBindingHost": "LLM 绑定主机",
|
||||||
|
"llmModel": "LLM 模型",
|
||||||
|
"maxTokens": "最大 Token 数",
|
||||||
|
"embeddingConfig": "嵌入配置",
|
||||||
|
"embeddingBinding": "嵌入绑定",
|
||||||
|
"embeddingBindingHost": "嵌入绑定主机",
|
||||||
|
"embeddingModel": "嵌入模型",
|
||||||
|
"storageConfig": "存储配置",
|
||||||
|
"kvStorage": "KV 存储",
|
||||||
|
"docStatusStorage": "文档状态存储",
|
||||||
|
"graphStorage": "图存储",
|
||||||
|
"vectorStorage": "向量存储"
|
||||||
|
},
|
||||||
|
"propertiesView": {
|
||||||
|
"node": {
|
||||||
|
"title": "节点",
|
||||||
|
"id": "ID",
|
||||||
|
"labels": "标签",
|
||||||
|
"degree": "度数",
|
||||||
|
"properties": "属性",
|
||||||
|
"relationships": "关系"
|
||||||
|
},
|
||||||
|
"edge": {
|
||||||
|
"title": "关系",
|
||||||
|
"id": "ID",
|
||||||
|
"type": "类型",
|
||||||
|
"source": "源",
|
||||||
|
"target": "目标",
|
||||||
|
"properties": "属性"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "搜索节点...",
|
||||||
|
"message": "以及其它 {count} 项"
|
||||||
|
},
|
||||||
|
"graphLabels": {
|
||||||
|
"selectTooltip": "选择查询标签",
|
||||||
|
"noLabels": "未找到标签",
|
||||||
|
"label": "标签",
|
||||||
|
"placeholder": "搜索标签...",
|
||||||
|
"andOthers": "以及其它 {count} 个"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"retrievePanel": {
|
||||||
|
"chatMessage": {
|
||||||
|
"copyTooltip": "复制到剪贴板",
|
||||||
|
"copyError": "无法复制文本到剪贴板"
|
||||||
|
},
|
||||||
|
|
||||||
|
"retrieval": {
|
||||||
|
"startPrompt": "在下面输入您的查询以开始检索",
|
||||||
|
"clear": "清除",
|
||||||
|
"send": "发送",
|
||||||
|
"placeholder": "输入您的查询...",
|
||||||
|
"error": "错误:无法获取响应"
|
||||||
|
},
|
||||||
|
"querySettings": {
|
||||||
|
"parametersTitle": "参数设置",
|
||||||
|
"parametersDescription": "配置查询参数",
|
||||||
|
|
||||||
|
"queryMode": "查询模式",
|
||||||
|
"queryModeTooltip": "选择检索策略:\n• 朴素:不使用高级技术的基本搜索\n• 本地:基于上下文的信息检索\n• 全局:利用全局知识库\n• 混合:结合本地和全局检索\n• 综合:集成知识图谱与向量检索",
|
||||||
|
"queryModeOptions": {
|
||||||
|
"naive": "朴素",
|
||||||
|
"local": "本地",
|
||||||
|
"global": "全局",
|
||||||
|
"hybrid": "混合",
|
||||||
|
"mix": "综合"
|
||||||
|
},
|
||||||
|
|
||||||
|
"responseFormat": "响应格式",
|
||||||
|
"responseFormatTooltip": "定义响应格式。例如:\n• 多个段落\n• 单个段落\n• 项目符号",
|
||||||
|
"responseFormatOptions": {
|
||||||
|
"multipleParagraphs": "多个段落",
|
||||||
|
"singleParagraph": "单个段落",
|
||||||
|
"bulletPoints": "项目符号"
|
||||||
|
},
|
||||||
|
|
||||||
|
"topK": "Top K 结果数",
|
||||||
|
"topKTooltip": "要检索的前 K 个项目数量。在“本地”模式下表示实体,在“全局”模式下表示关系",
|
||||||
|
"topKPlaceholder": "结果数",
|
||||||
|
|
||||||
|
"maxTokensTextUnit": "文本单元最大 Token 数",
|
||||||
|
"maxTokensTextUnitTooltip": "每个检索到的文本块允许的最大 Token 数",
|
||||||
|
|
||||||
|
"maxTokensGlobalContext": "全局上下文最大 Token 数",
|
||||||
|
"maxTokensGlobalContextTooltip": "在全局检索中为关系描述分配的最大 Token 数",
|
||||||
|
|
||||||
|
"maxTokensLocalContext": "本地上下文最大 Token 数",
|
||||||
|
"maxTokensLocalContextTooltip": "在本地检索中为实体描述分配的最大 Token 数",
|
||||||
|
|
||||||
|
"historyTurns": "历史轮次",
|
||||||
|
"historyTurnsTooltip": "在响应上下文中考虑的完整对话轮次(用户-助手对)",
|
||||||
|
"historyTurnsPlaceholder": "历史轮次的数量",
|
||||||
|
|
||||||
|
"hlKeywords": "高级关键词",
|
||||||
|
"hlKeywordsTooltip": "检索时优先考虑的高级关键词。请用逗号分隔",
|
||||||
|
"hlkeywordsPlaceHolder": "输入关键词",
|
||||||
|
|
||||||
|
"llKeywords": "低级关键词",
|
||||||
|
"llKeywordsTooltip": "用于优化检索焦点的低级关键词。请用逗号分隔",
|
||||||
|
|
||||||
|
"onlyNeedContext": "仅需要上下文",
|
||||||
|
"onlyNeedContextTooltip": "如果为 True,则仅返回检索到的上下文,而不会生成回复",
|
||||||
|
|
||||||
|
"onlyNeedPrompt": "仅需要提示",
|
||||||
|
"onlyNeedPromptTooltip": "如果为 True,则仅返回生成的提示,而不会生成回复",
|
||||||
|
|
||||||
|
"streamResponse": "流式响应",
|
||||||
|
"streamResponseTooltip": "如果为 True,则启用流式输出以获得实时响应"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2,6 +2,8 @@ import { StrictMode } from 'react'
|
|||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
import "./i18n";
|
||||||
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
Reference in New Issue
Block a user