diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index ea19b426ed..694b92ec76 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -1078,6 +1078,28 @@ SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION = ( os.getenv('SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION', 'True').lower() == 'true' ) +#################################### +# KNOWLEDGE TOOLS +#################################### + + +def _int_env(name: str, default: int) -> int: + try: + return max(int(os.getenv(name) or default), 1) + except (ValueError, TypeError): + return default + + +# Total output of a single kb_exec call, whatever the command. +KB_EXEC_MAX_OUTPUT_CHARS = _int_env('KB_EXEC_MAX_OUTPUT_CHARS', 30_000) +# Files a single kb_exec grep may scan before it asks for a narrower scope. +KB_EXEC_MAX_GREP_FILES = _int_env('KB_EXEC_MAX_GREP_FILES', 200) +# Matching lines returned by kb_exec grep and grep_knowledge_files. +KNOWLEDGE_GREP_MAX_MATCHES = _int_env('KNOWLEDGE_GREP_MAX_MATCHES', 50) +# Characters returned by view_file / view_knowledge_file. +VIEW_FILE_MAX_CHARS = _int_env('VIEW_FILE_MAX_CHARS', 100_000) +VIEW_FILE_DEFAULT_MAX_CHARS = _int_env('VIEW_FILE_DEFAULT_MAX_CHARS', 10_000) + #################################### # TOOLS/FUNCTIONS PIP OPTIONS #################################### diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py index ebbc9148cc..03b18ca655 100644 --- a/backend/open_webui/tools/builtin.py +++ b/backend/open_webui/tools/builtin.py @@ -17,6 +17,11 @@ from typing import Literal, Optional from fastapi import HTTPException, Request from open_webui.config import RAG_EMBEDDING_QUERY_PREFIX +from open_webui.env import ( + KNOWLEDGE_GREP_MAX_MATCHES, + VIEW_FILE_DEFAULT_MAX_CHARS, + VIEW_FILE_MAX_CHARS, +) from open_webui.models.channels import Channel, ChannelMember, Channels from open_webui.models.chats import Chats from open_webui.models.config import Config @@ -2157,12 +2162,6 @@ async def search_knowledge_files( return json.dumps({'error': str(e)}) -# Hard cap for view_file / view_knowledge_file output -MAX_VIEW_FILE_CHARS = 100_000 -DEFAULT_VIEW_FILE_MAX_CHARS = 10_000 -MAX_GREP_RESULTS = 50 - - async def _get_accessible_chat_files( files: Optional[list[dict]], user: dict, @@ -2229,7 +2228,7 @@ def _grep_file_models( if matches(line): file_matches += 1 total_matches += 1 - if not count_only and len(results) < MAX_GREP_RESULTS: + if not count_only and len(results) < KNOWLEDGE_GREP_MAX_MATCHES: results.append(f'{file.id} {file.filename}:{i}: {line}') if file_matches > 0 and count_only: @@ -2244,8 +2243,8 @@ def _grep_file_models( return f'No matches for "{pattern}"' output = '\n'.join(results) - if total_matches > MAX_GREP_RESULTS: - output += f'\n[{MAX_GREP_RESULTS} of {total_matches} matches shown — use file_id to narrow]' + if total_matches > KNOWLEDGE_GREP_MAX_MATCHES: + output += f'\n[{KNOWLEDGE_GREP_MAX_MATCHES} of {total_matches} matches shown — use file_id to narrow]' return output @@ -2589,7 +2588,7 @@ async def grep_knowledge_files( async def view_file( file_id: str, offset: int = 0, - max_chars: int = DEFAULT_VIEW_FILE_MAX_CHARS, + max_chars: int = VIEW_FILE_DEFAULT_MAX_CHARS, line_numbers: bool = False, start_line: Optional[int] = None, end_line: Optional[int] = None, @@ -2602,7 +2601,7 @@ async def view_file( :param file_id: The ID of the file to retrieve :param offset: Character offset to start reading from (default: 0) - :param max_chars: Maximum characters to return (default: 10000, hard cap: 100000) + :param max_chars: Maximum characters to return (a server-side hard cap applies) :param line_numbers: If true, prefix each line with its 1-indexed line number :param start_line: Optional 1-indexed start line (overrides offset/max_chars when set) :param end_line: Optional 1-indexed end line (inclusive) @@ -2624,10 +2623,10 @@ async def view_file( try: max_chars = int(max_chars) except ValueError: - max_chars = DEFAULT_VIEW_FILE_MAX_CHARS + max_chars = VIEW_FILE_DEFAULT_MAX_CHARS # Enforce hard cap - max_chars = min(max(max_chars, 1), MAX_VIEW_FILE_CHARS) + max_chars = min(max(max_chars, 1), VIEW_FILE_MAX_CHARS) offset = max(offset, 0) try: @@ -2705,7 +2704,7 @@ async def view_file( async def view_knowledge_file( file_id: str, offset: int = 0, - max_chars: int = DEFAULT_VIEW_FILE_MAX_CHARS, + max_chars: int = VIEW_FILE_DEFAULT_MAX_CHARS, line_numbers: bool = False, start_line: Optional[int] = None, end_line: Optional[int] = None, @@ -2717,7 +2716,7 @@ async def view_knowledge_file( :param file_id: The ID of the file to retrieve :param offset: Character offset to start reading from (default: 0) - :param max_chars: Maximum characters to return (default: 10000, hard cap: 100000) + :param max_chars: Maximum characters to return (a server-side hard cap applies) :param line_numbers: If true, prefix each line with its 1-indexed line number :param start_line: Optional 1-indexed start line (overrides offset/max_chars when set) :param end_line: Optional 1-indexed end line (inclusive) @@ -2739,10 +2738,10 @@ async def view_knowledge_file( try: max_chars = int(max_chars) except ValueError: - max_chars = DEFAULT_VIEW_FILE_MAX_CHARS + max_chars = VIEW_FILE_DEFAULT_MAX_CHARS # Enforce hard cap - max_chars = min(max(max_chars, 1), MAX_VIEW_FILE_CHARS) + max_chars = min(max(max_chars, 1), VIEW_FILE_MAX_CHARS) offset = max(offset, 0) try: diff --git a/backend/open_webui/tools/knowledge_fs.py b/backend/open_webui/tools/knowledge_fs.py index 419614c18e..eed252ff2b 100644 --- a/backend/open_webui/tools/knowledge_fs.py +++ b/backend/open_webui/tools/knowledge_fs.py @@ -19,15 +19,16 @@ from typing import Optional import regex from fastapi import Request +from open_webui.env import ( + KB_EXEC_MAX_GREP_FILES, + KB_EXEC_MAX_OUTPUT_CHARS, + KNOWLEDGE_GREP_MAX_MATCHES, +) + log = logging.getLogger(__name__) -# Limits -MAX_CAT_CHARS = 100_000 -DEFAULT_CAT_CHARS = 10_000 -MAX_GREP_FILES = 200 DEFAULT_HEAD_LINES = 10 DEFAULT_TAIL_LINES = 10 -MAX_GREP_MATCHES = 50 # Matching time allowed per tool call. Backtracking cost is exponential in the length of the # matched text, so capping the pattern or the line does not bound it. @@ -627,21 +628,10 @@ async def _kb_cat(args: list[str], flags: set[str], user: dict, model_knowledge: return resolved['error'] content = resolved['content'] - show_numbers = 'n' in flags - - if len(content) > MAX_CAT_CHARS: - content = content[:MAX_CAT_CHARS] - truncated = True - else: - truncated = False - - if show_numbers: + if 'n' in flags: lines = content.split('\n') content = '\n'.join(f'{i}: {line}' for i, line in enumerate(lines, 1)) - if truncated: - content += f'\n[truncated at {MAX_CAT_CHARS:,} chars — use head/tail/sed/grep to navigate]' - return content @@ -790,7 +780,7 @@ async def _kb_grep( if ext_filter: accessible = [f for f in accessible if f['filename'].endswith(f'.{ext_filter}')] - if len(accessible) > MAX_GREP_FILES: + if len(accessible) > KB_EXEC_MAX_GREP_FILES: return f'Too many files ({len(accessible)}). Scope your search: grep "{pattern}" docs/ or grep "{pattern}" *.py' from open_webui.models.files import Files @@ -822,7 +812,7 @@ async def _kb_grep( if not count_only and not filenames_only: for line_num, line_text in file_matches: - if len(results) < MAX_GREP_MATCHES: + if len(results) < KNOWLEDGE_GREP_MAX_MATCHES: results.append(f'{file_info["id"]} {file_info["filename"]}:{line_num}: {line_text.rstrip()}') if count_only: @@ -841,8 +831,8 @@ async def _kb_grep( return f'No matches for "{pattern}" across {len(accessible)} files' output = '\n'.join(results) - if total_matches > MAX_GREP_MATCHES: - output += f'\n[showing {MAX_GREP_MATCHES} of {total_matches} matches]' + if total_matches > KNOWLEDGE_GREP_MAX_MATCHES: + output += f'\n[showing {KNOWLEDGE_GREP_MAX_MATCHES} of {total_matches} matches]' return output @@ -1181,7 +1171,13 @@ async def kb_exec( # One budget for the whole command: a per-search budget would multiply by segment count. with match_budget(): - return await _execute_pipeline(segments, __user__, __model_knowledge__) + output = await _execute_pipeline(segments, __user__, __model_knowledge__) + if len(output) > KB_EXEC_MAX_OUTPUT_CHARS: + output = output[:KB_EXEC_MAX_OUTPUT_CHARS] + ( + f'\n[output truncated at {KB_EXEC_MAX_OUTPUT_CHARS:,} chars' + ' — narrow the command with a path, glob, head/tail/sed or grep]' + ) + return output except Exception as e: log.exception(f'kb_exec error: {e}') return f'Error: {e}'