feat: knowledge directory
This commit is contained in:
@@ -15,6 +15,8 @@ from open_webui.models.access_grants import AccessGrants
|
||||
from open_webui.models.files import FileMetadataResponse, FileModel, Files
|
||||
from open_webui.models.groups import Groups
|
||||
from open_webui.models.knowledge import (
|
||||
KnowledgeDirectoryForm,
|
||||
KnowledgeDirectoryModel,
|
||||
KnowledgeFileListResponse,
|
||||
KnowledgeForm,
|
||||
KnowledgeResponse,
|
||||
@@ -558,6 +560,7 @@ async def get_knowledge_files_by_id(
|
||||
view_option: str | None = None,
|
||||
order_by: str | None = None,
|
||||
direction: str | None = None,
|
||||
directory_id: str | None = Query(None, description='Filter by directory ID. Pass empty string for root.'),
|
||||
page: int | None = 1,
|
||||
user=Depends(get_verified_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
@@ -599,6 +602,9 @@ async def get_knowledge_files_by_id(
|
||||
filter['order_by'] = order_by
|
||||
if direction:
|
||||
filter['direction'] = direction
|
||||
# directory_id filtering: present in filter = scope to that directory (None = root)
|
||||
if directory_id is not None:
|
||||
filter['directory_id'] = directory_id if directory_id else None
|
||||
|
||||
return await Knowledges.search_files_by_id(id, user.id, filter=filter, skip=skip, limit=limit, db=db)
|
||||
|
||||
@@ -610,6 +616,7 @@ async def get_knowledge_files_by_id(
|
||||
|
||||
class KnowledgeFileIdForm(BaseModel):
|
||||
file_id: str
|
||||
directory_id: Optional[str] = None
|
||||
|
||||
|
||||
@router.post('/{id}/file/add', response_model=KnowledgeFilesResponse | None)
|
||||
@@ -673,7 +680,13 @@ async def add_file_to_knowledge_by_id(
|
||||
)
|
||||
|
||||
# Add file to knowledge base
|
||||
await Knowledges.add_file_to_knowledge_by_id(knowledge_id=id, file_id=form_data.file_id, user_id=user.id, db=db)
|
||||
await Knowledges.add_file_to_knowledge_by_id(
|
||||
knowledge_id=id,
|
||||
file_id=form_data.file_id,
|
||||
user_id=user.id,
|
||||
directory_id=form_data.directory_id,
|
||||
db=db,
|
||||
)
|
||||
except Exception as e:
|
||||
log.debug(e)
|
||||
raise HTTPException(
|
||||
@@ -1117,3 +1130,178 @@ async def export_knowledge_by_id(id: str, user=Depends(get_admin_user), db: Asyn
|
||||
media_type='application/zip',
|
||||
headers={'Content-Disposition': content_disposition},
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# Directory endpoints
|
||||
############################
|
||||
|
||||
|
||||
class KnowledgeDirectoryCreateForm(BaseModel):
|
||||
name: str
|
||||
parent_id: Optional[str] = None
|
||||
|
||||
|
||||
class KnowledgeDirectoryUpdateForm(BaseModel):
|
||||
name: Optional[str] = None
|
||||
parent_id: Optional[str] = '__unset__'
|
||||
|
||||
|
||||
class KnowledgeFileMoveForm(BaseModel):
|
||||
file_id: str
|
||||
directory_id: Optional[str] = None
|
||||
|
||||
|
||||
async def _verify_knowledge_write_access(
|
||||
id: str, user, db: AsyncSession
|
||||
):
|
||||
"""Verify the user has write access to the knowledge base. Returns the knowledge model."""
|
||||
knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db)
|
||||
if not knowledge:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
if (
|
||||
knowledge.user_id != user.id
|
||||
and not await AccessGrants.has_access(
|
||||
user_id=user.id,
|
||||
resource_type='knowledge',
|
||||
resource_id=knowledge.id,
|
||||
permission='write',
|
||||
db=db,
|
||||
)
|
||||
and user.role != 'admin'
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
return knowledge
|
||||
|
||||
|
||||
@router.post('/{id}/dirs/create', response_model=KnowledgeDirectoryModel)
|
||||
async def create_knowledge_directory(
|
||||
id: str,
|
||||
form_data: KnowledgeDirectoryCreateForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
await _verify_knowledge_write_access(id, user, db)
|
||||
|
||||
directory = await Knowledges.create_directory(
|
||||
knowledge_id=id,
|
||||
name=form_data.name,
|
||||
user_id=user.id,
|
||||
parent_id=form_data.parent_id,
|
||||
db=db,
|
||||
)
|
||||
if not directory:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Failed to create directory. A directory with this name may already exist at this level.',
|
||||
)
|
||||
return directory
|
||||
|
||||
|
||||
@router.post('/{id}/dirs/{dir_id}/update', response_model=KnowledgeDirectoryModel)
|
||||
async def update_knowledge_directory(
|
||||
id: str,
|
||||
dir_id: str,
|
||||
form_data: KnowledgeDirectoryUpdateForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
await _verify_knowledge_write_access(id, user, db)
|
||||
|
||||
# Verify directory belongs to this knowledge base
|
||||
directory = await Knowledges.get_directory_by_id(dir_id, db=db)
|
||||
if not directory or directory.knowledge_id != id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
result = await Knowledges.update_directory(
|
||||
directory_id=dir_id,
|
||||
name=form_data.name,
|
||||
parent_id=form_data.parent_id,
|
||||
db=db,
|
||||
)
|
||||
if not result:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Failed to update directory. This may be caused by a naming conflict or circular move.',
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.delete('/{id}/dirs/{dir_id}/delete')
|
||||
async def delete_knowledge_directory(
|
||||
id: str,
|
||||
dir_id: str,
|
||||
move_files: bool = Query(True, description='If true, move contained files to parent. If false, delete them.'),
|
||||
user=Depends(get_verified_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
await _verify_knowledge_write_access(id, user, db)
|
||||
|
||||
# Verify directory belongs to this knowledge base
|
||||
directory = await Knowledges.get_directory_by_id(dir_id, db=db)
|
||||
if not directory or directory.knowledge_id != id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
success = await Knowledges.delete_directory(
|
||||
directory_id=dir_id,
|
||||
move_files_to_parent=move_files,
|
||||
db=db,
|
||||
)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to delete directory.',
|
||||
)
|
||||
return {'status': True}
|
||||
|
||||
|
||||
@router.post('/{id}/file/move')
|
||||
async def move_file_in_knowledge(
|
||||
id: str,
|
||||
form_data: KnowledgeFileMoveForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
await _verify_knowledge_write_access(id, user, db)
|
||||
|
||||
# Verify file belongs to this knowledge base
|
||||
if not await Knowledges.has_file(knowledge_id=id, file_id=form_data.file_id, db=db):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
# If target directory is set, verify it belongs to this knowledge base
|
||||
if form_data.directory_id:
|
||||
directory = await Knowledges.get_directory_by_id(form_data.directory_id, db=db)
|
||||
if not directory or directory.knowledge_id != id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail='Target directory not found.',
|
||||
)
|
||||
|
||||
success = await Knowledges.move_file_to_directory(
|
||||
knowledge_id=id,
|
||||
file_id=form_data.file_id,
|
||||
directory_id=form_data.directory_id,
|
||||
db=db,
|
||||
)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to move file.',
|
||||
)
|
||||
return {'status': True}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user