Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
This commit is contained in:
Timothy Jaeryang Baek 2026-07-27 04:00:46 -04:00
parent e8fda1c7a0
commit 11e61b69eb
3 changed files with 56 additions and 39 deletions

View file

@ -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
####################################

View file

@ -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:

View file

@ -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}'