fix: 修复 lightrag 序列化与 knowledge router 路径问题 Fixes: #584

- 修正 lightrag.py 中的序列化逻辑
- 修复 knowledge_router.py 路由路径
- 更新 roadmap.md 文档记录
This commit is contained in:
Wenjie Zhang 2026-03-30 15:39:49 +08:00
parent 4f6353d555
commit d1a02579bc
3 changed files with 205 additions and 177 deletions

View File

@ -1,3 +1,4 @@
import asyncio
import os
import traceback
from functools import partial
@ -34,6 +35,9 @@ class LightRagKB(KnowledgeBase):
# 存储 LightRAG 实例映射 {db_id: LightRAG}
self.instances: dict[str, LightRAG] = {}
self._db_write_locks: dict[str, asyncio.Lock] = {}
self._db_instance_locks: dict[str, asyncio.Lock] = {}
self._lock_guard = asyncio.Lock()
logger.info("LightRagKB initialized")
@ -197,20 +201,34 @@ class LightRagKB(KnowledgeBase):
if db_id not in self.databases_meta:
return None
try:
# 创建实例
rag = await self._create_kb_instance(db_id, {})
instance_lock = await self._get_db_instance_lock(db_id)
async with instance_lock:
if db_id in self.instances:
logger.info(f"Using cached LightRAG instance for {db_id}")
return self.instances[db_id]
# 异步初始化存储
await self._initialize_kb_instance(rag)
try:
# 创建实例
rag = await self._create_kb_instance(db_id, {})
self.instances[db_id] = rag
return rag
# 异步初始化存储
await self._initialize_kb_instance(rag)
except Exception as e:
logger.error(f"Failed to create LightRAG instance for {db_id}: {e}")
logger.error(f"Traceback: {traceback.format_exc()}")
return None
self.instances[db_id] = rag
return rag
except Exception as e:
logger.error(f"Failed to create LightRAG instance for {db_id}: {e}")
logger.error(f"Traceback: {traceback.format_exc()}")
return None
async def _get_db_write_lock(self, db_id: str) -> asyncio.Lock:
async with self._lock_guard:
return self._db_write_locks.setdefault(db_id, asyncio.Lock())
async def _get_db_instance_lock(self, db_id: str) -> asyncio.Lock:
async with self._lock_guard:
return self._db_instance_locks.setdefault(db_id, asyncio.Lock())
def _get_llm_func(self, llm_info: dict):
"""获取 LLM 函数"""
@ -299,163 +317,73 @@ class LightRagKB(KnowledgeBase):
if db_id not in self.databases_meta:
raise ValueError(f"Database {db_id} not found")
rag = await self._get_lightrag_instance(db_id)
if not rag:
raise ValueError(f"Failed to get LightRAG instance for {db_id}")
db_write_lock = await self._get_db_write_lock(db_id)
async with db_write_lock:
rag = await self._get_lightrag_instance(db_id)
if not rag:
raise ValueError(f"Failed to get LightRAG instance for {db_id}")
# Get file meta
if file_id not in self.files_meta:
raise ValueError(f"File {file_id} not found")
file_meta = self.files_meta[file_id]
# Validate current status - only allow indexing from these states
current_status = file_meta.get("status")
allowed_statuses = {
FileStatus.PARSED,
FileStatus.ERROR_INDEXING,
FileStatus.INDEXED, # For re-indexing
"done", # Legacy status
}
if current_status not in allowed_statuses:
raise ValueError(
f"Cannot index file with status '{current_status}'. "
f"File must be parsed first (status should be one of: {', '.join(allowed_statuses)})"
)
# Check markdown file exists
if not file_meta.get("markdown_file"):
raise ValueError("File has not been parsed yet (no markdown_file)")
# Clear previous error if any
if "error" in file_meta:
self.files_meta[file_id].pop("error", None)
# Update status and add to processing queue
self.files_meta[file_id]["status"] = FileStatus.INDEXING
self.files_meta[file_id]["updated_at"] = utc_isoformat()
if operator_id:
self.files_meta[file_id]["updated_by"] = operator_id
await self._persist_file(file_id)
# Add to processing queue
self._add_to_processing_queue(file_id)
try:
# Read markdown
markdown_content = await self._read_markdown_from_minio(file_meta["markdown_file"])
file_path = file_meta.get("path")
filename = file_meta.get("filename") or file_id
processing_params = resolve_chunk_processing_params(
kb_additional_params=self.databases_meta.get(db_id, {}).get("metadata"),
file_processing_params=file_meta.get("processing_params"),
)
self.files_meta[file_id]["processing_params"] = processing_params
await self._save_metadata()
chunks = chunk_markdown(markdown_content, file_id, filename, processing_params)
chunk_input, split_by_character, split_by_character_only = self._prepare_lightrag_insert_payload(chunks)
if not chunk_input:
chunk_input = markdown_content
# Clean up existing chunks if any (for re-indexing)
await self.delete_file_chunks_only(db_id, file_id)
# Insert
await rag.ainsert(
input=chunk_input,
ids=file_id,
file_paths=file_path,
split_by_character=split_by_character,
split_by_character_only=split_by_character_only,
)
await self._ensure_doc_processed(rag, file_id)
logger.info(
f"Indexed file {file_id} into LightRAG with {len(chunks)} chunks, "
f"chunk_preset_id={processing_params.get('chunk_preset_id')}"
)
# Update status
self.files_meta[file_id]["status"] = FileStatus.INDEXED
self.files_meta[file_id]["updated_at"] = utc_isoformat()
if operator_id:
self.files_meta[file_id]["updated_by"] = operator_id
await self._persist_file(file_id)
return self.files_meta[file_id]
except Exception as e:
logger.error(f"Indexing failed for {file_id}: {e}")
self.files_meta[file_id]["status"] = FileStatus.ERROR_INDEXING
self.files_meta[file_id]["error"] = str(e)
self.files_meta[file_id]["updated_at"] = utc_isoformat()
if operator_id:
self.files_meta[file_id]["updated_by"] = operator_id
await self._persist_file(file_id)
raise
finally:
# Remove from processing queue
self._remove_from_processing_queue(file_id)
async def update_content(self, db_id: str, file_ids: list[str], params: dict | None = None) -> list[dict]:
"""更新内容 - 根据file_ids重新解析文件并更新向量库"""
if db_id not in self.databases_meta:
raise ValueError(f"Database {db_id} not found")
rag = await self._get_lightrag_instance(db_id)
if not rag:
raise ValueError(f"Failed to get LightRAG instance for {db_id}")
# 处理默认参数
if params is None:
params = {}
processed_items_info = []
for file_id in file_ids:
# 从元数据中获取文件信息
# Get file meta
if file_id not in self.files_meta:
logger.warning(f"File {file_id} not found in metadata, skipping")
continue
raise ValueError(f"File {file_id} not found")
file_meta = self.files_meta[file_id]
file_path = file_meta.get("path")
if not file_path:
logger.warning(f"File path not found for {file_id}, skipping")
continue
# Validate current status - only allow indexing from these states
current_status = file_meta.get("status")
allowed_statuses = {
FileStatus.PARSED,
FileStatus.ERROR_INDEXING,
FileStatus.INDEXED, # For re-indexing
"done", # Legacy status
}
# 添加到处理队列
if current_status not in allowed_statuses:
raise ValueError(
f"Cannot index file with status '{current_status}'. "
f"File must be parsed first (status should be one of: {', '.join(allowed_statuses)})"
)
# Check markdown file exists
if not file_meta.get("markdown_file"):
raise ValueError("File has not been parsed yet (no markdown_file)")
# Clear previous error if any
if "error" in file_meta:
self.files_meta[file_id].pop("error", None)
# Update status and add to processing queue
self.files_meta[file_id]["status"] = FileStatus.INDEXING
self.files_meta[file_id]["updated_at"] = utc_isoformat()
if operator_id:
self.files_meta[file_id]["updated_by"] = operator_id
await self._persist_file(file_id)
# Add to processing queue
self._add_to_processing_queue(file_id)
try:
# 更新状态为处理中
resolved_params = resolve_chunk_processing_params(
kb_additional_params=self.databases_meta.get(db_id, {}).get("metadata"),
file_processing_params=self.files_meta[file_id].get("processing_params"),
request_params=params,
)
self.files_meta[file_id]["processing_params"] = resolved_params
self.files_meta[file_id]["status"] = "processing"
await self._persist_file(file_id)
# 重新解析文件为 markdown
params["image_bucket"] = "public"
params["image_prefix"] = f"{db_id}/kb-images"
markdown_content = await Parser.aparse(source=file_path, params=params)
markdown_content_lines = markdown_content[:100].replace("\n", " ")
logger.info(f"Markdown content: {markdown_content_lines}...")
# Read markdown
markdown_content = await self._read_markdown_from_minio(file_meta["markdown_file"])
file_path = file_meta.get("path")
filename = file_meta.get("filename") or file_id
chunks = chunk_markdown(markdown_content, file_id, filename, resolved_params)
chunk_input, split_by_character, split_by_character_only = self._prepare_lightrag_insert_payload(chunks)
processing_params = resolve_chunk_processing_params(
kb_additional_params=self.databases_meta.get(db_id, {}).get("metadata"),
file_processing_params=file_meta.get("processing_params"),
)
self.files_meta[file_id]["processing_params"] = processing_params
await self._save_metadata()
chunks = chunk_markdown(markdown_content, file_id, filename, processing_params)
chunk_input, split_by_character, split_by_character_only = self._prepare_lightrag_insert_payload(
chunks
)
if not chunk_input:
chunk_input = markdown_content
# 先删除现有的 LightRAG 数据仅删除chunks保留元数据
# Clean up existing chunks if any (for re-indexing)
await self.delete_file_chunks_only(db_id, file_id)
# 使用 LightRAG 重新插入内容
# Insert
await rag.ainsert(
input=chunk_input,
ids=file_id,
@ -465,39 +393,137 @@ class LightRagKB(KnowledgeBase):
)
await self._ensure_doc_processed(rag, file_id)
logger.info(f"Updated file {file_path} in LightRAG. Done.")
logger.info(
f"Indexed file {file_id} into LightRAG with {len(chunks)} chunks, "
f"chunk_preset_id={processing_params.get('chunk_preset_id')}"
)
# 更新元数据状态
self.files_meta[file_id]["status"] = "done"
# Update status
self.files_meta[file_id]["status"] = FileStatus.INDEXED
self.files_meta[file_id]["updated_at"] = utc_isoformat()
if operator_id:
self.files_meta[file_id]["updated_by"] = operator_id
await self._persist_file(file_id)
# 从处理队列中移除
self._remove_from_processing_queue(file_id)
# 返回更新后的文件信息
updated_file_meta = file_meta.copy()
updated_file_meta["status"] = "done"
updated_file_meta["file_id"] = file_id
processed_items_info.append(updated_file_meta)
return self.files_meta[file_id]
except Exception as e:
error_msg = str(e)
logger.error(f"更新file {file_path} 失败: {error_msg}, {traceback.format_exc()}")
self.files_meta[file_id]["status"] = "failed"
self.files_meta[file_id]["error"] = error_msg
logger.error(f"Indexing failed for {file_id}: {e}")
self.files_meta[file_id]["status"] = FileStatus.ERROR_INDEXING
self.files_meta[file_id]["error"] = str(e)
self.files_meta[file_id]["updated_at"] = utc_isoformat()
if operator_id:
self.files_meta[file_id]["updated_by"] = operator_id
await self._persist_file(file_id)
raise
# 从处理队列中移除
finally:
# Remove from processing queue
self._remove_from_processing_queue(file_id)
# 返回失败的文件信息
failed_file_meta = file_meta.copy()
failed_file_meta["status"] = "failed"
failed_file_meta["file_id"] = file_id
failed_file_meta["error"] = error_msg
processed_items_info.append(failed_file_meta)
async def update_content(self, db_id: str, file_ids: list[str], params: dict | None = None) -> list[dict]:
"""更新内容 - 根据file_ids重新解析文件并更新向量库"""
if db_id not in self.databases_meta:
raise ValueError(f"Database {db_id} not found")
return processed_items_info
db_write_lock = await self._get_db_write_lock(db_id)
async with db_write_lock:
rag = await self._get_lightrag_instance(db_id)
if not rag:
raise ValueError(f"Failed to get LightRAG instance for {db_id}")
# 处理默认参数
if params is None:
params = {}
processed_items_info = []
for file_id in file_ids:
# 从元数据中获取文件信息
if file_id not in self.files_meta:
logger.warning(f"File {file_id} not found in metadata, skipping")
continue
file_meta = self.files_meta[file_id]
file_path = file_meta.get("path")
if not file_path:
logger.warning(f"File path not found for {file_id}, skipping")
continue
# 添加到处理队列
self._add_to_processing_queue(file_id)
try:
# 更新状态为处理中
resolved_params = resolve_chunk_processing_params(
kb_additional_params=self.databases_meta.get(db_id, {}).get("metadata"),
file_processing_params=self.files_meta[file_id].get("processing_params"),
request_params=params,
)
self.files_meta[file_id]["processing_params"] = resolved_params
self.files_meta[file_id]["status"] = "processing"
await self._persist_file(file_id)
# 重新解析文件为 markdown
params["image_bucket"] = "public"
params["image_prefix"] = f"{db_id}/kb-images"
markdown_content = await Parser.aparse(source=file_path, params=params)
markdown_content_lines = markdown_content[:100].replace("\n", " ")
logger.info(f"Markdown content: {markdown_content_lines}...")
filename = file_meta.get("filename") or file_id
chunks = chunk_markdown(markdown_content, file_id, filename, resolved_params)
chunk_input, split_by_character, split_by_character_only = self._prepare_lightrag_insert_payload(
chunks
)
if not chunk_input:
chunk_input = markdown_content
# 先删除现有的 LightRAG 数据仅删除chunks保留元数据
await self.delete_file_chunks_only(db_id, file_id)
# 使用 LightRAG 重新插入内容
await rag.ainsert(
input=chunk_input,
ids=file_id,
file_paths=file_path,
split_by_character=split_by_character,
split_by_character_only=split_by_character_only,
)
await self._ensure_doc_processed(rag, file_id)
logger.info(f"Updated file {file_path} in LightRAG. Done.")
# 更新元数据状态
self.files_meta[file_id]["status"] = "done"
await self._persist_file(file_id)
# 从处理队列中移除
self._remove_from_processing_queue(file_id)
# 返回更新后的文件信息
updated_file_meta = file_meta.copy()
updated_file_meta["status"] = "done"
updated_file_meta["file_id"] = file_id
processed_items_info.append(updated_file_meta)
except Exception as e:
error_msg = str(e)
logger.error(f"更新file {file_path} 失败: {error_msg}, {traceback.format_exc()}")
self.files_meta[file_id]["status"] = "failed"
self.files_meta[file_id]["error"] = error_msg
await self._persist_file(file_id)
# 从处理队列中移除
self._remove_from_processing_queue(file_id)
# 返回失败的文件信息
failed_file_meta = file_meta.copy()
failed_file_meta["status"] = "failed"
failed_file_meta["file_id"] = file_id
failed_file_meta["error"] = error_msg
processed_items_info.append(failed_file_meta)
return processed_items_info
async def aquery(self, query_text: str, db_id: str, agent_call: bool = False, **kwargs) -> str:
"""异步查询知识库"""

View File

@ -392,6 +392,7 @@ async def add_documents(
try:
# 2. Parse file (PARSING -> PARSED)
file_meta = await knowledge_base.parse_file(db_id, file_id, operator_id=current_user.user_id)
added_files[item] = (file_id, file_meta)
processed_items.append(file_meta)
parse_success_count += 1
except Exception as parse_error:

View File

@ -65,6 +65,7 @@
- 修复前端工具图标与渲染匹配不准确的问题:工具管理列表与工具调用结果统一改为基于工具 `id` 的精确映射,避免模糊匹配导致的误渲染,未命中的工具不再显示默认扳手图标
- 修复 GitHub Pages 文档部署工作流失败:移除 `actions/setup-node@v4` 对不存在 `docs/package-lock.json` 的缓存依赖,并将 `docs` 目录安装命令从 `npm ci` 调整为 `npm install`,避免因未提交锁文件导致 CI 在依赖缓存和安装阶段直接失败
- 修正沙盒 provisioner backend 命名与配置说明:统一对外使用 `docker` / `kubernetes`,保留 `local` 作为兼容别名;同步清理 compose 中未生效的 provisioner 环境变量、补齐 K8s 相关变量注释,并更新沙盒架构文档中的默认模式与 backend 描述
- 修复 LightRAG 同库写入并发导致的入库失败:为 `index_file` / `update_content` 增加按知识库维度的串行锁,并补齐 `documents` 接口 `auto_index` 阶段对最新解析状态的回写与回归测试,避免长时间入库任务进行中再次选择同库文件时直接并发写入报错
<!-- 添加到这里 -->