refac
This commit is contained in:
@@ -1587,17 +1587,26 @@ async def search_knowledge_files(
|
|||||||
return json.dumps({'error': str(e)})
|
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
|
||||||
|
|
||||||
|
|
||||||
async def view_file(
|
async def view_file(
|
||||||
file_id: str,
|
file_id: str,
|
||||||
|
offset: int = 0,
|
||||||
|
max_chars: int = DEFAULT_VIEW_FILE_MAX_CHARS,
|
||||||
__request__: Request = None,
|
__request__: Request = None,
|
||||||
__user__: dict = None,
|
__user__: dict = None,
|
||||||
__model_knowledge__: Optional[list[dict]] = None,
|
__model_knowledge__: Optional[list[dict]] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Get the full content of a file by its ID.
|
Get the content of a file by its ID. Supports pagination for large files.
|
||||||
|
|
||||||
:param file_id: The ID of the file to retrieve
|
:param file_id: The ID of the file to retrieve
|
||||||
:return: JSON with the file's id, filename, and full text content
|
:param offset: Character offset to start reading from (default: 0)
|
||||||
|
:param max_chars: Maximum characters to return (default: 10000, hard cap: 100000)
|
||||||
|
:return: JSON with the file's id, filename, content, and pagination metadata if truncated
|
||||||
"""
|
"""
|
||||||
if __request__ is None:
|
if __request__ is None:
|
||||||
return json.dumps({'error': 'Request context not available'})
|
return json.dumps({'error': 'Request context not available'})
|
||||||
@@ -1605,6 +1614,22 @@ async def view_file(
|
|||||||
if not __user__:
|
if not __user__:
|
||||||
return json.dumps({'error': 'User context not available'})
|
return json.dumps({'error': 'User context not available'})
|
||||||
|
|
||||||
|
# Coerce parameters from LLM tool calls (may come as strings)
|
||||||
|
if isinstance(offset, str):
|
||||||
|
try:
|
||||||
|
offset = int(offset)
|
||||||
|
except ValueError:
|
||||||
|
offset = 0
|
||||||
|
if isinstance(max_chars, str):
|
||||||
|
try:
|
||||||
|
max_chars = int(max_chars)
|
||||||
|
except ValueError:
|
||||||
|
max_chars = DEFAULT_VIEW_FILE_MAX_CHARS
|
||||||
|
|
||||||
|
# Enforce hard cap
|
||||||
|
max_chars = min(max(max_chars, 1), MAX_VIEW_FILE_CHARS)
|
||||||
|
offset = max(offset, 0)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from open_webui.models.files import Files
|
from open_webui.models.files import Files
|
||||||
from open_webui.utils.access_control.files import has_access_to_file
|
from open_webui.utils.access_control.files import has_access_to_file
|
||||||
@@ -1634,16 +1659,27 @@ async def view_file(
|
|||||||
if file.data:
|
if file.data:
|
||||||
content = file.data.get('content', '')
|
content = file.data.get('content', '')
|
||||||
|
|
||||||
return json.dumps(
|
total_chars = len(content)
|
||||||
{
|
sliced = content[offset:offset + max_chars]
|
||||||
'id': file.id,
|
is_truncated = (offset + len(sliced)) < total_chars
|
||||||
'filename': file.filename,
|
|
||||||
'content': content,
|
result = {
|
||||||
'updated_at': file.updated_at,
|
'id': file.id,
|
||||||
'created_at': file.created_at,
|
'filename': file.filename,
|
||||||
},
|
'content': sliced,
|
||||||
ensure_ascii=False,
|
'updated_at': file.updated_at,
|
||||||
)
|
'created_at': file.created_at,
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_truncated or offset > 0:
|
||||||
|
result['truncated'] = is_truncated
|
||||||
|
result['total_chars'] = total_chars
|
||||||
|
result['returned_chars'] = len(sliced)
|
||||||
|
result['offset'] = offset
|
||||||
|
if is_truncated:
|
||||||
|
result['next_offset'] = offset + len(sliced)
|
||||||
|
|
||||||
|
return json.dumps(result, ensure_ascii=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f'view_file error: {e}')
|
log.exception(f'view_file error: {e}')
|
||||||
return json.dumps({'error': str(e)})
|
return json.dumps({'error': str(e)})
|
||||||
@@ -1651,14 +1687,18 @@ async def view_file(
|
|||||||
|
|
||||||
async def view_knowledge_file(
|
async def view_knowledge_file(
|
||||||
file_id: str,
|
file_id: str,
|
||||||
|
offset: int = 0,
|
||||||
|
max_chars: int = DEFAULT_VIEW_FILE_MAX_CHARS,
|
||||||
__request__: Request = None,
|
__request__: Request = None,
|
||||||
__user__: dict = None,
|
__user__: dict = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Get the full content of a file from a knowledge base.
|
Get the content of a file from a knowledge base. Supports pagination for large files.
|
||||||
|
|
||||||
:param file_id: The ID of the file to retrieve
|
:param file_id: The ID of the file to retrieve
|
||||||
:return: JSON with the file's id, filename, and full text content
|
:param offset: Character offset to start reading from (default: 0)
|
||||||
|
:param max_chars: Maximum characters to return (default: 10000, hard cap: 100000)
|
||||||
|
:return: JSON with the file's id, filename, content, and pagination metadata if truncated
|
||||||
"""
|
"""
|
||||||
if __request__ is None:
|
if __request__ is None:
|
||||||
return json.dumps({'error': 'Request context not available'})
|
return json.dumps({'error': 'Request context not available'})
|
||||||
@@ -1666,6 +1706,22 @@ async def view_knowledge_file(
|
|||||||
if not __user__:
|
if not __user__:
|
||||||
return json.dumps({'error': 'User context not available'})
|
return json.dumps({'error': 'User context not available'})
|
||||||
|
|
||||||
|
# Coerce parameters from LLM tool calls (may come as strings)
|
||||||
|
if isinstance(offset, str):
|
||||||
|
try:
|
||||||
|
offset = int(offset)
|
||||||
|
except ValueError:
|
||||||
|
offset = 0
|
||||||
|
if isinstance(max_chars, str):
|
||||||
|
try:
|
||||||
|
max_chars = int(max_chars)
|
||||||
|
except ValueError:
|
||||||
|
max_chars = DEFAULT_VIEW_FILE_MAX_CHARS
|
||||||
|
|
||||||
|
# Enforce hard cap
|
||||||
|
max_chars = min(max(max_chars, 1), MAX_VIEW_FILE_CHARS)
|
||||||
|
offset = max(offset, 0)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from open_webui.models.files import Files
|
from open_webui.models.files import Files
|
||||||
from open_webui.models.knowledge import Knowledges
|
from open_webui.models.knowledge import Knowledges
|
||||||
@@ -1708,10 +1764,14 @@ async def view_knowledge_file(
|
|||||||
if file.data:
|
if file.data:
|
||||||
content = file.data.get('content', '')
|
content = file.data.get('content', '')
|
||||||
|
|
||||||
|
total_chars = len(content)
|
||||||
|
sliced = content[offset:offset + max_chars]
|
||||||
|
is_truncated = (offset + len(sliced)) < total_chars
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'id': file.id,
|
'id': file.id,
|
||||||
'filename': file.filename,
|
'filename': file.filename,
|
||||||
'content': content,
|
'content': sliced,
|
||||||
'updated_at': file.updated_at,
|
'updated_at': file.updated_at,
|
||||||
'created_at': file.created_at,
|
'created_at': file.created_at,
|
||||||
}
|
}
|
||||||
@@ -1719,12 +1779,240 @@ async def view_knowledge_file(
|
|||||||
result['knowledge_id'] = knowledge_info['id']
|
result['knowledge_id'] = knowledge_info['id']
|
||||||
result['knowledge_name'] = knowledge_info['name']
|
result['knowledge_name'] = knowledge_info['name']
|
||||||
|
|
||||||
|
if is_truncated or offset > 0:
|
||||||
|
result['truncated'] = is_truncated
|
||||||
|
result['total_chars'] = total_chars
|
||||||
|
result['returned_chars'] = len(sliced)
|
||||||
|
result['offset'] = offset
|
||||||
|
if is_truncated:
|
||||||
|
result['next_offset'] = offset + len(sliced)
|
||||||
|
|
||||||
return json.dumps(result, ensure_ascii=False)
|
return json.dumps(result, ensure_ascii=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f'view_knowledge_file error: {e}')
|
log.exception(f'view_knowledge_file error: {e}')
|
||||||
return json.dumps({'error': str(e)})
|
return json.dumps({'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
async def list_attached_knowledge(
|
||||||
|
__request__: Request = None,
|
||||||
|
__user__: dict = None,
|
||||||
|
__model_knowledge__: Optional[list[dict]] = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
List all knowledge bases, files, and notes attached to the current model.
|
||||||
|
Use this first to discover what knowledge is available before querying or reading files.
|
||||||
|
|
||||||
|
:return: JSON with knowledge_bases, files, and notes attached to this model
|
||||||
|
"""
|
||||||
|
if __request__ is None:
|
||||||
|
return json.dumps({'error': 'Request context not available'})
|
||||||
|
|
||||||
|
if not __user__:
|
||||||
|
return json.dumps({'error': 'User context not available'})
|
||||||
|
|
||||||
|
if not __model_knowledge__:
|
||||||
|
return json.dumps({'knowledge_bases': [], 'files': [], 'notes': []})
|
||||||
|
|
||||||
|
try:
|
||||||
|
from open_webui.models.knowledge import Knowledges
|
||||||
|
from open_webui.models.files import Files
|
||||||
|
from open_webui.models.notes import Notes
|
||||||
|
from open_webui.models.access_grants import AccessGrants
|
||||||
|
|
||||||
|
user_id = __user__.get('id')
|
||||||
|
user_role = __user__.get('role', 'user')
|
||||||
|
user_group_ids = [group.id for group in Groups.get_groups_by_member_id(user_id)]
|
||||||
|
|
||||||
|
knowledge_bases = []
|
||||||
|
files = []
|
||||||
|
notes = []
|
||||||
|
|
||||||
|
for item in __model_knowledge__:
|
||||||
|
item_type = item.get('type')
|
||||||
|
item_id = item.get('id')
|
||||||
|
|
||||||
|
if item_type == 'collection':
|
||||||
|
knowledge = Knowledges.get_knowledge_by_id(item_id)
|
||||||
|
if knowledge and (
|
||||||
|
user_role == 'admin'
|
||||||
|
or knowledge.user_id == user_id
|
||||||
|
or AccessGrants.has_access(
|
||||||
|
user_id=user_id,
|
||||||
|
resource_type='knowledge',
|
||||||
|
resource_id=knowledge.id,
|
||||||
|
permission='read',
|
||||||
|
user_group_ids=set(user_group_ids),
|
||||||
|
)
|
||||||
|
):
|
||||||
|
kb_files = Knowledges.get_files_by_id(knowledge.id)
|
||||||
|
file_count = len(kb_files) if kb_files else 0
|
||||||
|
|
||||||
|
kb_entry = {
|
||||||
|
'id': knowledge.id,
|
||||||
|
'name': knowledge.name,
|
||||||
|
'description': knowledge.description or '',
|
||||||
|
'file_count': file_count,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Include file listing for each KB
|
||||||
|
if kb_files:
|
||||||
|
kb_entry['files'] = [
|
||||||
|
{'id': f.id, 'filename': f.filename}
|
||||||
|
for f in kb_files
|
||||||
|
]
|
||||||
|
|
||||||
|
knowledge_bases.append(kb_entry)
|
||||||
|
|
||||||
|
elif item_type == 'file':
|
||||||
|
file = Files.get_file_by_id(item_id)
|
||||||
|
if file:
|
||||||
|
files.append({
|
||||||
|
'id': file.id,
|
||||||
|
'filename': file.filename,
|
||||||
|
'updated_at': file.updated_at,
|
||||||
|
})
|
||||||
|
|
||||||
|
elif item_type == 'note':
|
||||||
|
note = Notes.get_note_by_id(item_id)
|
||||||
|
if note and (
|
||||||
|
user_role == 'admin'
|
||||||
|
or note.user_id == user_id
|
||||||
|
or AccessGrants.has_access(
|
||||||
|
user_id=user_id,
|
||||||
|
resource_type='note',
|
||||||
|
resource_id=note.id,
|
||||||
|
permission='read',
|
||||||
|
)
|
||||||
|
):
|
||||||
|
notes.append({
|
||||||
|
'id': note.id,
|
||||||
|
'title': note.title,
|
||||||
|
})
|
||||||
|
|
||||||
|
return json.dumps({
|
||||||
|
'knowledge_bases': knowledge_bases,
|
||||||
|
'files': files,
|
||||||
|
'notes': notes,
|
||||||
|
}, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f'list_attached_knowledge error: {e}')
|
||||||
|
return json.dumps({'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
async def search_attached_files(
|
||||||
|
query: str,
|
||||||
|
knowledge_id: Optional[str] = None,
|
||||||
|
count: int = 10,
|
||||||
|
skip: int = 0,
|
||||||
|
__request__: Request = None,
|
||||||
|
__user__: dict = None,
|
||||||
|
__model_knowledge__: Optional[list[dict]] = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Search files by filename within the attached knowledge scope.
|
||||||
|
Only searches knowledge bases and files that are attached to the current model.
|
||||||
|
|
||||||
|
:param query: The filename search query
|
||||||
|
:param knowledge_id: Optional KB id to limit search to a specific attached knowledge base
|
||||||
|
:param count: Maximum number of results to return (default: 10)
|
||||||
|
:param skip: Number of results to skip for pagination (default: 0)
|
||||||
|
:return: JSON with matching files containing id, filename, knowledge_id, and knowledge_name
|
||||||
|
"""
|
||||||
|
if __request__ is None:
|
||||||
|
return json.dumps({'error': 'Request context not available'})
|
||||||
|
|
||||||
|
if not __user__:
|
||||||
|
return json.dumps({'error': 'User context not available'})
|
||||||
|
|
||||||
|
if not __model_knowledge__:
|
||||||
|
return json.dumps([])
|
||||||
|
|
||||||
|
try:
|
||||||
|
from open_webui.models.knowledge import Knowledges
|
||||||
|
from open_webui.models.files import Files
|
||||||
|
from open_webui.models.access_grants import AccessGrants
|
||||||
|
|
||||||
|
user_id = __user__.get('id')
|
||||||
|
user_role = __user__.get('role', 'user')
|
||||||
|
user_group_ids = [group.id for group in Groups.get_groups_by_member_id(user_id)]
|
||||||
|
|
||||||
|
# Collect attached KB IDs and direct file IDs
|
||||||
|
attached_kb_ids = set()
|
||||||
|
attached_file_ids = set()
|
||||||
|
|
||||||
|
for item in __model_knowledge__:
|
||||||
|
item_type = item.get('type')
|
||||||
|
item_id = item.get('id')
|
||||||
|
if item_type == 'collection':
|
||||||
|
attached_kb_ids.add(item_id)
|
||||||
|
elif item_type == 'file':
|
||||||
|
attached_file_ids.add(item_id)
|
||||||
|
|
||||||
|
# If knowledge_id is specified, verify it's in the attached set
|
||||||
|
if knowledge_id:
|
||||||
|
if knowledge_id not in attached_kb_ids:
|
||||||
|
return json.dumps({'error': f'Knowledge base {knowledge_id} is not attached to this model'})
|
||||||
|
attached_kb_ids = {knowledge_id}
|
||||||
|
|
||||||
|
all_files = []
|
||||||
|
|
||||||
|
# Search within attached KBs
|
||||||
|
for kb_id in attached_kb_ids:
|
||||||
|
knowledge = Knowledges.get_knowledge_by_id(kb_id)
|
||||||
|
if not knowledge:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not (
|
||||||
|
user_role == 'admin'
|
||||||
|
or knowledge.user_id == user_id
|
||||||
|
or AccessGrants.has_access(
|
||||||
|
user_id=user_id,
|
||||||
|
resource_type='knowledge',
|
||||||
|
resource_id=knowledge.id,
|
||||||
|
permission='read',
|
||||||
|
user_group_ids=set(user_group_ids),
|
||||||
|
)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
result = Knowledges.search_files_by_id(
|
||||||
|
knowledge_id=kb_id,
|
||||||
|
user_id=user_id,
|
||||||
|
filter={'query': query},
|
||||||
|
skip=0,
|
||||||
|
limit=count + skip, # Fetch enough for pagination across KBs
|
||||||
|
)
|
||||||
|
|
||||||
|
for file in result.items:
|
||||||
|
all_files.append({
|
||||||
|
'id': file.id,
|
||||||
|
'filename': file.filename,
|
||||||
|
'knowledge_id': knowledge.id,
|
||||||
|
'knowledge_name': knowledge.name,
|
||||||
|
'updated_at': file.updated_at,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Search within directly attached files (filename match)
|
||||||
|
if not knowledge_id and attached_file_ids:
|
||||||
|
query_lower = query.lower() if query else ''
|
||||||
|
for file_id in attached_file_ids:
|
||||||
|
file = Files.get_file_by_id(file_id)
|
||||||
|
if file and (not query_lower or query_lower in file.filename.lower()):
|
||||||
|
all_files.append({
|
||||||
|
'id': file.id,
|
||||||
|
'filename': file.filename,
|
||||||
|
'updated_at': file.updated_at,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Apply pagination across combined results
|
||||||
|
all_files = all_files[skip:skip + count]
|
||||||
|
|
||||||
|
return json.dumps(all_files, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f'search_attached_files error: {e}')
|
||||||
|
return json.dumps({'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
async def query_knowledge_files(
|
async def query_knowledge_files(
|
||||||
query: str,
|
query: str,
|
||||||
knowledge_ids: Optional[list[str]] = None,
|
knowledge_ids: Optional[list[str]] = None,
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ def get_citation_source_from_tool_result(
|
|||||||
Returns a list of sources (usually one, but query_knowledge_files may return multiple).
|
Returns a list of sources (usually one, but query_knowledge_files may return multiple).
|
||||||
"""
|
"""
|
||||||
_EXPECTS_LIST = {'search_web', 'query_knowledge_files'}
|
_EXPECTS_LIST = {'search_web', 'query_knowledge_files'}
|
||||||
_EXPECTS_DICT = {'view_knowledge_file'}
|
_EXPECTS_DICT = {'view_knowledge_file', 'view_file'}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
@@ -271,7 +271,7 @@ def get_citation_source_from_tool_result(
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
elif tool_name == 'view_knowledge_file':
|
elif tool_name in ('view_knowledge_file', 'view_file'):
|
||||||
file_data = tool_result
|
file_data = tool_result
|
||||||
filename = file_data.get('filename', 'Unknown File')
|
filename = file_data.get('filename', 'Unknown File')
|
||||||
file_id = file_data.get('id', '')
|
file_id = file_data.get('id', '')
|
||||||
@@ -4143,6 +4143,7 @@ async def streaming_chat_response_handler(response, ctx):
|
|||||||
in [
|
in [
|
||||||
'search_web',
|
'search_web',
|
||||||
'fetch_url',
|
'fetch_url',
|
||||||
|
'view_file',
|
||||||
'view_knowledge_file',
|
'view_knowledge_file',
|
||||||
'query_knowledge_files',
|
'query_knowledge_files',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ from open_webui.tools.builtin import (
|
|||||||
query_knowledge_bases,
|
query_knowledge_bases,
|
||||||
search_knowledge_files,
|
search_knowledge_files,
|
||||||
query_knowledge_files,
|
query_knowledge_files,
|
||||||
|
list_attached_knowledge,
|
||||||
|
search_attached_files,
|
||||||
view_file,
|
view_file,
|
||||||
view_knowledge_file,
|
view_knowledge_file,
|
||||||
view_skill,
|
view_skill,
|
||||||
@@ -405,12 +407,15 @@ def get_builtin_tools(
|
|||||||
model_knowledge = list(model_knowledge or []) + list(folder_knowledge)
|
model_knowledge = list(model_knowledge or []) + list(folder_knowledge)
|
||||||
if is_builtin_tool_enabled('knowledge'):
|
if is_builtin_tool_enabled('knowledge'):
|
||||||
if model_knowledge:
|
if model_knowledge:
|
||||||
# Model has attached knowledge - only allow semantic search within it
|
# Model has attached knowledge - provide discovery, search and semantic tools
|
||||||
|
builtin_functions.append(list_attached_knowledge)
|
||||||
|
builtin_functions.append(search_attached_files)
|
||||||
builtin_functions.append(query_knowledge_files)
|
builtin_functions.append(query_knowledge_files)
|
||||||
|
|
||||||
knowledge_types = {item.get('type') for item in model_knowledge}
|
knowledge_types = {item.get('type') for item in model_knowledge}
|
||||||
if 'file' in knowledge_types or 'collection' in knowledge_types:
|
if 'file' in knowledge_types or 'collection' in knowledge_types:
|
||||||
builtin_functions.append(view_file)
|
builtin_functions.append(view_file)
|
||||||
|
builtin_functions.append(view_knowledge_file)
|
||||||
if 'note' in knowledge_types:
|
if 'note' in knowledge_types:
|
||||||
builtin_functions.append(view_note)
|
builtin_functions.append(view_note)
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user