feat: knowledge directory

This commit is contained in:
Timothy Jaeryang Baek
2026-05-13 22:37:53 +09:00
parent 8ace73da84
commit c2cbc47ca7
10 changed files with 1244 additions and 13 deletions
+189 -1
View File
@@ -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}