mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-20 06:31:02 +08:00
Feat: add skills space to context engine (#13908)
### What problem does this PR solve? issue #13714 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@@ -187,7 +187,9 @@ async def delete(tenant_id: str = None):
|
||||
return get_error_argument_result(err)
|
||||
|
||||
try:
|
||||
success, result = await file_api_service.delete_files(tenant_id, req["ids"])
|
||||
# Get Authorization header to pass to Go backend
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
success, result = await file_api_service.delete_files(tenant_id, req["ids"], auth_header)
|
||||
if success:
|
||||
return get_result(data=result)
|
||||
else:
|
||||
|
||||
@@ -67,14 +67,14 @@ async def upload_file(tenant_id: str, pf_id: str, file_objs: list):
|
||||
if not e:
|
||||
return False, "Folder not found!"
|
||||
last_folder = await thread_pool_exec(
|
||||
FileService.create_folder, file, file_id_list[len_id_list - 1], file_obj_names, len_id_list
|
||||
FileService.create_folder, file, file_id_list[len_id_list - 1], file_obj_names, len_id_list, tenant_id, tenant_id
|
||||
)
|
||||
else:
|
||||
e, file = await thread_pool_exec(FileService.get_by_id, file_id_list[len_id_list - 2])
|
||||
if not e:
|
||||
return False, "Folder not found!"
|
||||
last_folder = await thread_pool_exec(
|
||||
FileService.create_folder, file, file_id_list[len_id_list - 2], file_obj_names, len_id_list
|
||||
FileService.create_folder, file, file_id_list[len_id_list - 2], file_obj_names, len_id_list, tenant_id, tenant_id
|
||||
)
|
||||
|
||||
filetype = filename_type(file_obj_names[file_len - 1])
|
||||
@@ -158,6 +158,7 @@ def list_files(tenant_id: str, args: dict):
|
||||
root_folder = FileService.get_root_folder(tenant_id)
|
||||
pf_id = root_folder["id"]
|
||||
FileService.init_knowledgebase_docs(pf_id, tenant_id)
|
||||
FileService.init_skills_folder(pf_id, tenant_id)
|
||||
|
||||
e, file = FileService.get_by_id(pf_id)
|
||||
if not e:
|
||||
@@ -203,17 +204,110 @@ def get_all_parent_folders(file_id: str):
|
||||
return True, {"parent_folders": [pf.to_json() for pf in parent_folders]}
|
||||
|
||||
|
||||
async def delete_files(uid: str, file_ids: list):
|
||||
async def delete_files(uid: str, file_ids: list, auth_header: str = ""):
|
||||
"""
|
||||
Delete files/folders with team permission check and recursive deletion.
|
||||
|
||||
:param uid: user ID
|
||||
:param file_ids: list of file IDs to delete
|
||||
:param auth_header: Authorization header for Go backend API calls
|
||||
:return: (success, result) or (success, error_message)
|
||||
"""
|
||||
errors: list[str] = []
|
||||
success_count = 0
|
||||
|
||||
def _get_space_uuid_by_name(tenant_id, space_name, authorization):
|
||||
"""Get space UUID by space name from Go backend"""
|
||||
try:
|
||||
import requests
|
||||
|
||||
host = getattr(settings, 'HOST_IP', '127.0.0.1')
|
||||
# Go service runs on port+4 (9384 by default)
|
||||
port = getattr(settings, 'HOST_PORT', 9380) + 4
|
||||
service_url = f"http://{host}:{port}"
|
||||
|
||||
# List all spaces and find the one matching the name
|
||||
url = f"{service_url}/api/v1/skills/spaces"
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if authorization:
|
||||
headers["Authorization"] = authorization
|
||||
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
if data.get("code") == 0:
|
||||
spaces = data.get("data", {}).get("spaces", [])
|
||||
for space in spaces:
|
||||
if space.get("name") == space_name:
|
||||
return space.get("id")
|
||||
except Exception as e:
|
||||
logging.warning(f"Error getting space UUID: {e}")
|
||||
return None
|
||||
|
||||
def _delete_skill_index(tenant_id, space_name, skill_name, authorization):
|
||||
"""Delete skill index from Go backend.
|
||||
|
||||
Returns:
|
||||
bool: True if deletion succeeded (HTTP 200), False otherwise.
|
||||
"""
|
||||
try:
|
||||
import requests
|
||||
from urllib.parse import quote
|
||||
|
||||
# Construct service URL from settings
|
||||
host = getattr(settings, 'HOST_IP', '127.0.0.1')
|
||||
# Go service runs on port+4 (9384 by default)
|
||||
port = getattr(settings, 'HOST_PORT', 9380) + 4
|
||||
service_url = f"http://{host}:{port}"
|
||||
|
||||
# Get space UUID from space name
|
||||
space_uuid = _get_space_uuid_by_name(tenant_id, space_name, authorization)
|
||||
space_id = space_uuid if space_uuid else space_name
|
||||
|
||||
url = f"{service_url}/api/v1/skills/index?skill_id={quote(skill_name)}&space_id={quote(space_id)}"
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if authorization:
|
||||
headers["Authorization"] = authorization
|
||||
|
||||
response = requests.delete(url, headers=headers, timeout=10)
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
data = response.json()
|
||||
if data.get("code") == 0:
|
||||
logging.info(
|
||||
f"Successfully deleted skill index: space={space_name}, skill={skill_name}, "
|
||||
f"status={response.status_code}, code=0"
|
||||
)
|
||||
return True
|
||||
else:
|
||||
app_code = data.get("code", "unknown")
|
||||
app_msg = data.get("message", "no message")
|
||||
logging.error(
|
||||
f"Failed to delete skill index: space={space_name}, skill={skill_name}, "
|
||||
f"status={response.status_code}, app_code={app_code}, app_msg={app_msg}, "
|
||||
f"response={response.text}"
|
||||
)
|
||||
return False
|
||||
except ValueError as json_err:
|
||||
# JSON decode error - treat as failure
|
||||
logging.error(
|
||||
f"Failed to parse delete response JSON: space={space_name}, skill={skill_name}, "
|
||||
f"error={json_err}, raw_response={response.text}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
logging.error(
|
||||
f"Failed to delete skill index: space={space_name}, skill={skill_name}, "
|
||||
f"status={response.status_code}, response={response.text}"
|
||||
)
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Exception deleting skill index: space={space_name}, skill={skill_name}, error={e}"
|
||||
)
|
||||
return False
|
||||
|
||||
def _delete_single_file(file) -> int:
|
||||
try:
|
||||
if file.location:
|
||||
@@ -254,9 +348,64 @@ async def delete_files(uid: str, file_ids: list):
|
||||
|
||||
return 0
|
||||
|
||||
def _delete_folder_recursive(folder, tenant_id):
|
||||
def _find_ancestor_skill_space(folder_id, tenant_id):
|
||||
"""Walk up the folder hierarchy to find an ancestor with source_type == 'skill_space'.
|
||||
|
||||
Returns:
|
||||
tuple: (success, folder) where folder has source_type == 'skill_space', or (False, None)
|
||||
"""
|
||||
visited = set()
|
||||
current_id = folder_id
|
||||
while current_id and current_id not in visited:
|
||||
visited.add(current_id)
|
||||
success, folder = FileService.get_by_id(current_id)
|
||||
if not success or not folder:
|
||||
return False, None
|
||||
if folder.source_type == "skill_space":
|
||||
return True, folder
|
||||
# Move to parent
|
||||
current_id = folder.parent_id
|
||||
return False, None
|
||||
|
||||
def _delete_folder_recursive(folder, tenant_id) -> int:
|
||||
deleted = 0
|
||||
current_space_name = None
|
||||
is_space_folder = folder.source_type == "skill_space"
|
||||
is_skill_folder = False
|
||||
|
||||
if not is_space_folder:
|
||||
parent_success, parent_folder = FileService.get_by_id(folder.parent_id)
|
||||
if parent_success and parent_folder and parent_folder.source_type == "skill_space":
|
||||
is_skill_folder = True
|
||||
current_space_name = parent_folder.name
|
||||
logging.info(f"Identified skill folder '{folder.name}' (parent space: {current_space_name})")
|
||||
else:
|
||||
ancestor_success, ancestor_folder = _find_ancestor_skill_space(folder.parent_id, tenant_id)
|
||||
if ancestor_success and ancestor_folder:
|
||||
is_skill_folder = True
|
||||
current_space_name = ancestor_folder.name
|
||||
logging.info(f"Identified skill folder '{folder.name}' (ancestor space: {current_space_name})")
|
||||
|
||||
if is_space_folder:
|
||||
current_space_name = folder.name
|
||||
logging.info(f"Processing space folder '{folder.name}' - will delete all skill indexes within")
|
||||
|
||||
if is_skill_folder and current_space_name and not is_space_folder:
|
||||
logging.info(f"Deleting skill index for skill '{folder.name}' in space '{current_space_name}'")
|
||||
index_deleted = _delete_skill_index(tenant_id, current_space_name, folder.name, auth_header)
|
||||
if not index_deleted:
|
||||
logging.error(
|
||||
f"Aborting folder deletion due to index deletion failure: "
|
||||
f"folder={folder.name}, space={current_space_name}"
|
||||
)
|
||||
errors.append(
|
||||
f"Failed to delete skill index for folder '{folder.name}' in space '{current_space_name}'. "
|
||||
f"Folder deletion aborted to prevent orphaned indexes."
|
||||
)
|
||||
return deleted
|
||||
sub_files = FileService.list_all_files_by_parent_id(folder.id)
|
||||
logging.info(f"Folder '{folder.name}': found {len(sub_files)} children to delete")
|
||||
|
||||
for sub_file in sub_files:
|
||||
if sub_file.type == FileType.FOLDER.value:
|
||||
deleted += _delete_folder_recursive(sub_file, tenant_id)
|
||||
@@ -269,6 +418,16 @@ async def delete_files(uid: str, file_ids: list):
|
||||
errors.append(f"Failed to delete folder record {folder.id}: {e}")
|
||||
else:
|
||||
deleted += 1
|
||||
|
||||
try:
|
||||
if hasattr(settings.STORAGE_IMPL, 'remove_bucket'):
|
||||
logging.info(f"Removing storage bucket for folder '{folder.name}' (id={folder.id})")
|
||||
settings.STORAGE_IMPL.remove_bucket(folder.id)
|
||||
else:
|
||||
logging.debug(f"Storage implementation does not support remove_bucket, skipping for folder '{folder.name}'")
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to remove storage bucket for folder '{folder.name}' (id={folder.id}): {e}")
|
||||
|
||||
return deleted
|
||||
|
||||
def _rm_sync():
|
||||
@@ -288,6 +447,9 @@ async def delete_files(uid: str, file_ids: list):
|
||||
if file.source_type == FileSource.KNOWLEDGEBASE:
|
||||
continue
|
||||
|
||||
if file.source_type == "skill_space":
|
||||
continue
|
||||
|
||||
if file.type == FileType.FOLDER.value:
|
||||
success_count += _delete_folder_recursive(file, uid)
|
||||
continue
|
||||
|
||||
@@ -74,3 +74,4 @@ PIPELINE_SPECIAL_PROGRESS_FREEZE_TASK_TYPES = {PipelineTaskType.RAPTOR.lower(),
|
||||
|
||||
|
||||
KNOWLEDGEBASE_FOLDER_NAME=".knowledgebase"
|
||||
SKILLS_FOLDER_NAME="skills"
|
||||
|
||||
@@ -28,7 +28,7 @@ logger = logging.getLogger(__name__)
|
||||
import xxhash
|
||||
from peewee import fn
|
||||
|
||||
from api.db import KNOWLEDGEBASE_FOLDER_NAME, FileType
|
||||
from api.db import KNOWLEDGEBASE_FOLDER_NAME, SKILLS_FOLDER_NAME, FileType
|
||||
from api.db.db_models import DB, Document, File, File2Document, Knowledgebase, Task
|
||||
from api.db.services import duplicate_name
|
||||
from api.db.services.common_service import CommonService
|
||||
@@ -191,23 +191,24 @@ class FileService(CommonService):
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def create_folder(cls, file, parent_id, name, count):
|
||||
from api.apps import current_user
|
||||
def create_folder(cls, file, parent_id, name, count, tenant_id, created_by):
|
||||
# Recursively create folder structure
|
||||
# Args:
|
||||
# file: Current file object
|
||||
# parent_id: Parent folder ID
|
||||
# name: List of folder names to create
|
||||
# count: Current depth in creation
|
||||
# tenant_id: Tenant ID
|
||||
# created_by: Created by user ID
|
||||
# Returns:
|
||||
# Created file object
|
||||
if count > len(name) - 2:
|
||||
return file
|
||||
else:
|
||||
file = cls.insert(
|
||||
{"id": get_uuid(), "parent_id": parent_id, "tenant_id": current_user.id, "created_by": current_user.id, "name": name[count], "location": "", "size": 0, "type": FileType.FOLDER.value}
|
||||
{"id": get_uuid(), "parent_id": parent_id, "tenant_id": tenant_id, "created_by": created_by, "name": name[count], "location": "", "size": 0, "type": FileType.FOLDER.value}
|
||||
)
|
||||
return cls.create_folder(file, file.id, name, count + 1)
|
||||
return cls.create_folder(file, file.id, name, count + 1, tenant_id, created_by)
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
@@ -293,6 +294,28 @@ class FileService(CommonService):
|
||||
cls.save(**file)
|
||||
return file
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def init_skills_folder(cls, root_id, tenant_id):
|
||||
# Initialize skills folder if not exists
|
||||
# Args:
|
||||
# root_id: Root folder ID
|
||||
# tenant_id: Tenant ID
|
||||
for _ in cls.model.select().where((cls.model.name == SKILLS_FOLDER_NAME) & (cls.model.parent_id == root_id)):
|
||||
return
|
||||
file_id = get_uuid()
|
||||
file = {
|
||||
"id": file_id,
|
||||
"parent_id": root_id,
|
||||
"tenant_id": tenant_id,
|
||||
"created_by": tenant_id,
|
||||
"name": SKILLS_FOLDER_NAME,
|
||||
"type": FileType.FOLDER.value,
|
||||
"size": 0,
|
||||
"location": "",
|
||||
}
|
||||
cls.save(**file)
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def init_knowledgebase_docs(cls, root_id, tenant_id):
|
||||
|
||||
Reference in New Issue
Block a user