@@ -1587,24 +1587,49 @@ async def search_knowledge_files(
15871587 return json .dumps ({'error' : str (e )})
15881588
15891589
1590+ # Hard cap for view_file / view_knowledge_file output
1591+ MAX_VIEW_FILE_CHARS = 100_000
1592+ DEFAULT_VIEW_FILE_MAX_CHARS = 10_000
1593+
1594+
15901595async def view_file (
15911596 file_id : str ,
1597+ offset : int = 0 ,
1598+ max_chars : int = DEFAULT_VIEW_FILE_MAX_CHARS ,
15921599 __request__ : Request = None ,
15931600 __user__ : dict = None ,
15941601 __model_knowledge__ : Optional [list [dict ]] = None ,
15951602) -> str :
15961603 """
1597- Get the full content of a file by its ID.
1604+ Get the content of a file by its ID. Supports pagination for large files .
15981605
15991606 :param file_id: The ID of the file to retrieve
1600- :return: JSON with the file's id, filename, and full text content
1607+ :param offset: Character offset to start reading from (default: 0)
1608+ :param max_chars: Maximum characters to return (default: 10000, hard cap: 100000)
1609+ :return: JSON with the file's id, filename, content, and pagination metadata if truncated
16011610 """
16021611 if __request__ is None :
16031612 return json .dumps ({'error' : 'Request context not available' })
16041613
16051614 if not __user__ :
16061615 return json .dumps ({'error' : 'User context not available' })
16071616
1617+ # Coerce parameters from LLM tool calls (may come as strings)
1618+ if isinstance (offset , str ):
1619+ try :
1620+ offset = int (offset )
1621+ except ValueError :
1622+ offset = 0
1623+ if isinstance (max_chars , str ):
1624+ try :
1625+ max_chars = int (max_chars )
1626+ except ValueError :
1627+ max_chars = DEFAULT_VIEW_FILE_MAX_CHARS
1628+
1629+ # Enforce hard cap
1630+ max_chars = min (max (max_chars , 1 ), MAX_VIEW_FILE_CHARS )
1631+ offset = max (offset , 0 )
1632+
16081633 try :
16091634 from open_webui .models .files import Files
16101635 from open_webui .utils .access_control .files import has_access_to_file
@@ -1634,38 +1659,69 @@ async def view_file(
16341659 if file .data :
16351660 content = file .data .get ('content' , '' )
16361661
1637- return json .dumps (
1638- {
1639- 'id' : file .id ,
1640- 'filename' : file .filename ,
1641- 'content' : content ,
1642- 'updated_at' : file .updated_at ,
1643- 'created_at' : file .created_at ,
1644- },
1645- ensure_ascii = False ,
1646- )
1662+ total_chars = len (content )
1663+ sliced = content [offset :offset + max_chars ]
1664+ is_truncated = (offset + len (sliced )) < total_chars
1665+
1666+ result = {
1667+ 'id' : file .id ,
1668+ 'filename' : file .filename ,
1669+ 'content' : sliced ,
1670+ 'updated_at' : file .updated_at ,
1671+ 'created_at' : file .created_at ,
1672+ }
1673+
1674+ if is_truncated or offset > 0 :
1675+ result ['truncated' ] = is_truncated
1676+ result ['total_chars' ] = total_chars
1677+ result ['returned_chars' ] = len (sliced )
1678+ result ['offset' ] = offset
1679+ if is_truncated :
1680+ result ['next_offset' ] = offset + len (sliced )
1681+
1682+ return json .dumps (result , ensure_ascii = False )
16471683 except Exception as e :
16481684 log .exception (f'view_file error: { e } ' )
16491685 return json .dumps ({'error' : str (e )})
16501686
16511687
16521688async def view_knowledge_file (
16531689 file_id : str ,
1690+ offset : int = 0 ,
1691+ max_chars : int = DEFAULT_VIEW_FILE_MAX_CHARS ,
16541692 __request__ : Request = None ,
16551693 __user__ : dict = None ,
16561694) -> str :
16571695 """
1658- Get the full content of a file from a knowledge base.
1696+ Get the content of a file from a knowledge base. Supports pagination for large files .
16591697
16601698 :param file_id: The ID of the file to retrieve
1661- :return: JSON with the file's id, filename, and full text content
1699+ :param offset: Character offset to start reading from (default: 0)
1700+ :param max_chars: Maximum characters to return (default: 10000, hard cap: 100000)
1701+ :return: JSON with the file's id, filename, content, and pagination metadata if truncated
16621702 """
16631703 if __request__ is None :
16641704 return json .dumps ({'error' : 'Request context not available' })
16651705
16661706 if not __user__ :
16671707 return json .dumps ({'error' : 'User context not available' })
16681708
1709+ # Coerce parameters from LLM tool calls (may come as strings)
1710+ if isinstance (offset , str ):
1711+ try :
1712+ offset = int (offset )
1713+ except ValueError :
1714+ offset = 0
1715+ if isinstance (max_chars , str ):
1716+ try :
1717+ max_chars = int (max_chars )
1718+ except ValueError :
1719+ max_chars = DEFAULT_VIEW_FILE_MAX_CHARS
1720+
1721+ # Enforce hard cap
1722+ max_chars = min (max (max_chars , 1 ), MAX_VIEW_FILE_CHARS )
1723+ offset = max (offset , 0 )
1724+
16691725 try :
16701726 from open_webui .models .files import Files
16711727 from open_webui .models .knowledge import Knowledges
@@ -1708,23 +1764,255 @@ async def view_knowledge_file(
17081764 if file .data :
17091765 content = file .data .get ('content' , '' )
17101766
1767+ total_chars = len (content )
1768+ sliced = content [offset :offset + max_chars ]
1769+ is_truncated = (offset + len (sliced )) < total_chars
1770+
17111771 result = {
17121772 'id' : file .id ,
17131773 'filename' : file .filename ,
1714- 'content' : content ,
1774+ 'content' : sliced ,
17151775 'updated_at' : file .updated_at ,
17161776 'created_at' : file .created_at ,
17171777 }
17181778 if knowledge_info :
17191779 result ['knowledge_id' ] = knowledge_info ['id' ]
17201780 result ['knowledge_name' ] = knowledge_info ['name' ]
17211781
1782+ if is_truncated or offset > 0 :
1783+ result ['truncated' ] = is_truncated
1784+ result ['total_chars' ] = total_chars
1785+ result ['returned_chars' ] = len (sliced )
1786+ result ['offset' ] = offset
1787+ if is_truncated :
1788+ result ['next_offset' ] = offset + len (sliced )
1789+
17221790 return json .dumps (result , ensure_ascii = False )
17231791 except Exception as e :
17241792 log .exception (f'view_knowledge_file error: { e } ' )
17251793 return json .dumps ({'error' : str (e )})
17261794
17271795
1796+ async def list_attached_knowledge (
1797+ __request__ : Request = None ,
1798+ __user__ : dict = None ,
1799+ __model_knowledge__ : Optional [list [dict ]] = None ,
1800+ ) -> str :
1801+ """
1802+ List all knowledge bases, files, and notes attached to the current model.
1803+ Use this first to discover what knowledge is available before querying or reading files.
1804+
1805+ :return: JSON with knowledge_bases, files, and notes attached to this model
1806+ """
1807+ if __request__ is None :
1808+ return json .dumps ({'error' : 'Request context not available' })
1809+
1810+ if not __user__ :
1811+ return json .dumps ({'error' : 'User context not available' })
1812+
1813+ if not __model_knowledge__ :
1814+ return json .dumps ({'knowledge_bases' : [], 'files' : [], 'notes' : []})
1815+
1816+ try :
1817+ from open_webui .models .knowledge import Knowledges
1818+ from open_webui .models .files import Files
1819+ from open_webui .models .notes import Notes
1820+ from open_webui .models .access_grants import AccessGrants
1821+
1822+ user_id = __user__ .get ('id' )
1823+ user_role = __user__ .get ('role' , 'user' )
1824+ user_group_ids = [group .id for group in Groups .get_groups_by_member_id (user_id )]
1825+
1826+ knowledge_bases = []
1827+ files = []
1828+ notes = []
1829+
1830+ for item in __model_knowledge__ :
1831+ item_type = item .get ('type' )
1832+ item_id = item .get ('id' )
1833+
1834+ if item_type == 'collection' :
1835+ knowledge = Knowledges .get_knowledge_by_id (item_id )
1836+ if knowledge and (
1837+ user_role == 'admin'
1838+ or knowledge .user_id == user_id
1839+ or AccessGrants .has_access (
1840+ user_id = user_id ,
1841+ resource_type = 'knowledge' ,
1842+ resource_id = knowledge .id ,
1843+ permission = 'read' ,
1844+ user_group_ids = set (user_group_ids ),
1845+ )
1846+ ):
1847+ kb_files = Knowledges .get_files_by_id (knowledge .id )
1848+ file_count = len (kb_files ) if kb_files else 0
1849+
1850+ kb_entry = {
1851+ 'id' : knowledge .id ,
1852+ 'name' : knowledge .name ,
1853+ 'description' : knowledge .description or '' ,
1854+ 'file_count' : file_count ,
1855+ }
1856+
1857+ # Include file listing for each KB
1858+ if kb_files :
1859+ kb_entry ['files' ] = [
1860+ {'id' : f .id , 'filename' : f .filename }
1861+ for f in kb_files
1862+ ]
1863+
1864+ knowledge_bases .append (kb_entry )
1865+
1866+ elif item_type == 'file' :
1867+ file = Files .get_file_by_id (item_id )
1868+ if file :
1869+ files .append ({
1870+ 'id' : file .id ,
1871+ 'filename' : file .filename ,
1872+ 'updated_at' : file .updated_at ,
1873+ })
1874+
1875+ elif item_type == 'note' :
1876+ note = Notes .get_note_by_id (item_id )
1877+ if note and (
1878+ user_role == 'admin'
1879+ or note .user_id == user_id
1880+ or AccessGrants .has_access (
1881+ user_id = user_id ,
1882+ resource_type = 'note' ,
1883+ resource_id = note .id ,
1884+ permission = 'read' ,
1885+ )
1886+ ):
1887+ notes .append ({
1888+ 'id' : note .id ,
1889+ 'title' : note .title ,
1890+ })
1891+
1892+ return json .dumps ({
1893+ 'knowledge_bases' : knowledge_bases ,
1894+ 'files' : files ,
1895+ 'notes' : notes ,
1896+ }, ensure_ascii = False )
1897+ except Exception as e :
1898+ log .exception (f'list_attached_knowledge error: { e } ' )
1899+ return json .dumps ({'error' : str (e )})
1900+
1901+
1902+ async def search_attached_files (
1903+ query : str ,
1904+ knowledge_id : Optional [str ] = None ,
1905+ count : int = 10 ,
1906+ skip : int = 0 ,
1907+ __request__ : Request = None ,
1908+ __user__ : dict = None ,
1909+ __model_knowledge__ : Optional [list [dict ]] = None ,
1910+ ) -> str :
1911+ """
1912+ Search files by filename within the attached knowledge scope.
1913+ Only searches knowledge bases and files that are attached to the current model.
1914+
1915+ :param query: The filename search query
1916+ :param knowledge_id: Optional KB id to limit search to a specific attached knowledge base
1917+ :param count: Maximum number of results to return (default: 10)
1918+ :param skip: Number of results to skip for pagination (default: 0)
1919+ :return: JSON with matching files containing id, filename, knowledge_id, and knowledge_name
1920+ """
1921+ if __request__ is None :
1922+ return json .dumps ({'error' : 'Request context not available' })
1923+
1924+ if not __user__ :
1925+ return json .dumps ({'error' : 'User context not available' })
1926+
1927+ if not __model_knowledge__ :
1928+ return json .dumps ([])
1929+
1930+ try :
1931+ from open_webui .models .knowledge import Knowledges
1932+ from open_webui .models .files import Files
1933+ from open_webui .models .access_grants import AccessGrants
1934+
1935+ user_id = __user__ .get ('id' )
1936+ user_role = __user__ .get ('role' , 'user' )
1937+ user_group_ids = [group .id for group in Groups .get_groups_by_member_id (user_id )]
1938+
1939+ # Collect attached KB IDs and direct file IDs
1940+ attached_kb_ids = set ()
1941+ attached_file_ids = set ()
1942+
1943+ for item in __model_knowledge__ :
1944+ item_type = item .get ('type' )
1945+ item_id = item .get ('id' )
1946+ if item_type == 'collection' :
1947+ attached_kb_ids .add (item_id )
1948+ elif item_type == 'file' :
1949+ attached_file_ids .add (item_id )
1950+
1951+ # If knowledge_id is specified, verify it's in the attached set
1952+ if knowledge_id :
1953+ if knowledge_id not in attached_kb_ids :
1954+ return json .dumps ({'error' : f'Knowledge base { knowledge_id } is not attached to this model' })
1955+ attached_kb_ids = {knowledge_id }
1956+
1957+ all_files = []
1958+
1959+ # Search within attached KBs
1960+ for kb_id in attached_kb_ids :
1961+ knowledge = Knowledges .get_knowledge_by_id (kb_id )
1962+ if not knowledge :
1963+ continue
1964+
1965+ if not (
1966+ user_role == 'admin'
1967+ or knowledge .user_id == user_id
1968+ or AccessGrants .has_access (
1969+ user_id = user_id ,
1970+ resource_type = 'knowledge' ,
1971+ resource_id = knowledge .id ,
1972+ permission = 'read' ,
1973+ user_group_ids = set (user_group_ids ),
1974+ )
1975+ ):
1976+ continue
1977+
1978+ result = Knowledges .search_files_by_id (
1979+ knowledge_id = kb_id ,
1980+ user_id = user_id ,
1981+ filter = {'query' : query },
1982+ skip = 0 ,
1983+ limit = count + skip , # Fetch enough for pagination across KBs
1984+ )
1985+
1986+ for file in result .items :
1987+ all_files .append ({
1988+ 'id' : file .id ,
1989+ 'filename' : file .filename ,
1990+ 'knowledge_id' : knowledge .id ,
1991+ 'knowledge_name' : knowledge .name ,
1992+ 'updated_at' : file .updated_at ,
1993+ })
1994+
1995+ # Search within directly attached files (filename match)
1996+ if not knowledge_id and attached_file_ids :
1997+ query_lower = query .lower () if query else ''
1998+ for file_id in attached_file_ids :
1999+ file = Files .get_file_by_id (file_id )
2000+ if file and (not query_lower or query_lower in file .filename .lower ()):
2001+ all_files .append ({
2002+ 'id' : file .id ,
2003+ 'filename' : file .filename ,
2004+ 'updated_at' : file .updated_at ,
2005+ })
2006+
2007+ # Apply pagination across combined results
2008+ all_files = all_files [skip :skip + count ]
2009+
2010+ return json .dumps (all_files , ensure_ascii = False )
2011+ except Exception as e :
2012+ log .exception (f'search_attached_files error: { e } ' )
2013+ return json .dumps ({'error' : str (e )})
2014+
2015+
17282016async def query_knowledge_files (
17292017 query : str ,
17302018 knowledge_ids : Optional [list [str ]] = None ,
0 commit comments