From 1b67da70043c758334ea85b8f184661b1faf027d Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Wed, 9 Sep 2026 18:30:47 +0200 Subject: [PATCH] feat: sort flags for kb_exec file listings (#29840) kb_exec silently ignored ls -t. It accepted the flag, dropped it and returned the same undefined database order as a plain ls, so the model believed it had a newest-first list when it did not. With a few thousand files in a knowledge base there was no way to ask what changed recently without reading the whole listing and comparing dates by eye. ls, tree and find now sort their file lines by name by default, so the same knowledge base always lists the same way. -t sorts newest first, -S largest first and -r reverses, combinable like -at or -tr, matching the flags the model already knows from a shell. Directories keep their existing name order and stay grouped first. No query changes: the timestamps and sizes were already loaded for the date and size columns. --- backend/open_webui/tools/knowledge_fs.py | 27 ++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/backend/open_webui/tools/knowledge_fs.py b/backend/open_webui/tools/knowledge_fs.py index c12ed1da3c..d44b2b0d8c 100644 --- a/backend/open_webui/tools/knowledge_fs.py +++ b/backend/open_webui/tools/knowledge_fs.py @@ -587,7 +587,7 @@ async def _kb_ls(args: list[str], flags: set[str], user: dict, model_knowledge: if flat_mode: # Flat mode: build full tree (legitimate use) tree = await _build_directory_tree(kb_id) - for f in tree['files']: + for f in _sort_files(tree['files'], flags): lines.append(f' {f["id"]} {f["path"]} {_fmt_size(f)} {_fmt_date(f)}') lines.append('') continue @@ -610,7 +610,7 @@ async def _kb_ls(args: list[str], flags: set[str], user: dict, model_knowledge: # Show files at this level (filter from accessible files) accessible = await _get_accessible_files(user, model_knowledge, knowledge_id=kb_id) dir_files = [f for f in accessible if f['directory_id'] == target_dir_id] - for f in dir_files: + for f in _sort_files(dir_files, flags): lines.append(f' {f["id"]} {f["filename"]} {_fmt_size(f)} {_fmt_date(f)}') if not subdirs and not dir_files: @@ -619,13 +619,24 @@ async def _kb_ls(args: list[str], flags: set[str], user: dict, model_knowledge: if direct_files and not target_kb_id and not dir_path: lines.append('Attached Files:') - for f in direct_files: + for f in _sort_files(direct_files, flags): lines.append(f' {f["id"]} {f["filename"]} {_fmt_size(f)} {_fmt_date(f)}') lines.append('') return '\n'.join(lines).rstrip() +def _sort_files(files: list[dict], flags: set[str]) -> list[dict]: + """ls-style ordering: by name or path, -t newest first, -S largest first, -r reverses.""" + if 't' in flags: + files = sorted(files, key=lambda f: f.get('updated_at') or 0, reverse=True) + elif 'S' in flags: + files = sorted(files, key=lambda f: f.get('size') or 0, reverse=True) + else: + files = sorted(files, key=lambda f: f.get('path') or f['filename']) + return files[::-1] if 'r' in flags else files + + def _fmt_size(f: dict) -> str: return f'{f["size"]:,} bytes' if f.get('size') else '' @@ -893,7 +904,7 @@ async def _kb_find(args: list[str], flags: set[str], user: dict, model_knowledge return f'No files matching "{pattern}"{scope_str}' lines = [] - for f in matched: + for f in _sort_files(matched, flags): kb_info = f' ({f["knowledge_name"]})' if f.get('knowledge_name') else '' lines.append(f'{f["id"]} {f["filename"]}{kb_info}') return '\n'.join(lines) @@ -1065,7 +1076,7 @@ async def _kb_tree(args: list[str], flags: set[str], user: dict, model_knowledge def _render_tree(parent_id, prefix=' '): items = [] subdirs = _get_subdirs(tree, parent_id) - files = _get_files_in_dir(tree, parent_id) + files = _sort_files(_get_files_in_dir(tree, parent_id), flags) entries = [('dir', d) for d in subdirs] + [('file', f) for f in files] for idx, (etype, entry) in enumerate(entries): @@ -1090,7 +1101,7 @@ async def _kb_tree(args: list[str], flags: set[str], user: dict, model_knowledge if direct_files and not dir_scope: output.append('Attached Files:') - for idx, f in enumerate(direct_files): + for idx, f in enumerate(_sort_files(direct_files, flags)): connector = '└── ' if idx == len(direct_files) - 1 else '├── ' output.append(f' {connector}{f["filename"]}') output.append(f'\n 0 directories, {len(direct_files)} files') @@ -1158,6 +1169,9 @@ async def kb_exec( ls — list root files and directories ls docs/ — list contents of a directory ls -a — flat list of all files with full paths + ls -t — newest modified first + ls -S — largest first + ls -r — reverse file order tree — recursive directory tree view tree docs/ — subtree from a directory cat -n — read file with line numbers @@ -1172,6 +1186,7 @@ async def kb_exec( grep "text" *.py — filter by extension find "*.md" — find files by glob find docs/ "*.md" — find within a directory + find -t "*.md", tree -t — same sort flags as ls wc — line/word/char counts stat — file metadata