mirror of
https://github.com/open-webui/open-webui.git
synced 2026-09-26 01:10:37 -04:00
feat: let operators expose chosen file metadata to the model in retrieved sources (#29696)
Custom metadata attached to a file upload now reaches the vector DB, but the model still never sees it. Both prompt-assembly paths build their output from a fixed field set: the classic RAG <source> tag carries only id, name and resource type, and the retrieval tools return only content, source and file id per chunk. A scraper that records where each document came from therefore cannot get that origin in front of the model, so answers cannot state it. RAG_SOURCE_METADATA_KEYS names the chunk metadata keys allowed through to the model. Configured keys are emitted as extra attributes on the <source> tag and as extra fields on tool result chunks, covering both retrieval paths. It is empty by default, so nothing changes for existing deployments. An allowlist instead of passing everything through, because chunk metadata also carries file hashes, collection names, embedding config and relevance scores, which would then be added to every retrieved chunk of every request. Values are attacker-controllable through an uploaded file, so they are escaped before they go into the tag, and a configured key can never displace a field the tag or the chunk already defines. Reported in open-webui/open-webui#29486.
This commit is contained in:
@@ -22,6 +22,9 @@ ENABLE_RAG_CSV_SUMMARY=false
|
||||
# Set to true to preserve backing file records, storage blobs, and per-file vectors when files are removed from knowledge bases.
|
||||
ENABLE_KNOWLEDGE_FILE_RETENTION=false
|
||||
|
||||
# Comma-separated chunk metadata keys to expose to the model alongside retrieved content.
|
||||
RAG_SOURCE_METADATA_KEYS=''
|
||||
|
||||
# Set to false to disable workspace Tools and Functions.
|
||||
ENABLE_PLUGINS=true
|
||||
|
||||
|
||||
@@ -371,6 +371,9 @@ ENABLE_QUERIES_CACHE = os.getenv('ENABLE_QUERIES_CACHE', 'False').lower() == 'tr
|
||||
ENABLE_ADMIN_CHAT_ACCESS = os.getenv('ENABLE_ADMIN_CHAT_ACCESS', 'True').lower() == 'true'
|
||||
RAG_SYSTEM_CONTEXT = os.getenv('RAG_SYSTEM_CONTEXT', 'False').lower() == 'true'
|
||||
|
||||
# Empty by default: chunk metadata also holds internal bookkeeping (file hashes, collection names, scores).
|
||||
RAG_SOURCE_METADATA_KEYS = [key.strip() for key in os.getenv('RAG_SOURCE_METADATA_KEYS', '').split(',') if key.strip()]
|
||||
|
||||
####################################
|
||||
# REDIS
|
||||
####################################
|
||||
|
||||
@@ -34,6 +34,7 @@ from open_webui.env import (
|
||||
ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS,
|
||||
MPS_INFERENCE_LOCK,
|
||||
OFFLINE_MODE,
|
||||
RAG_SOURCE_METADATA_KEYS,
|
||||
USE_SLIM,
|
||||
)
|
||||
from open_webui.models.access_grants import AccessGrants
|
||||
@@ -1361,6 +1362,11 @@ async def filter_accessible_collections(
|
||||
return validated
|
||||
|
||||
|
||||
def filter_source_metadata(metadata: dict) -> dict:
|
||||
"""Keep only the chunk metadata keys the operator allowed the model to see."""
|
||||
return {key: metadata[key] for key in RAG_SOURCE_METADATA_KEYS if metadata.get(key) is not None}
|
||||
|
||||
|
||||
async def get_sources_from_items(
|
||||
request,
|
||||
items,
|
||||
|
||||
@@ -29,7 +29,7 @@ from open_webui.models.memories import Memories
|
||||
from open_webui.models.messages import Message, Messages
|
||||
from open_webui.models.notes import Notes
|
||||
from open_webui.models.users import UserModel
|
||||
from open_webui.retrieval.utils import get_content_from_url
|
||||
from open_webui.retrieval.utils import filter_source_metadata, get_content_from_url
|
||||
from open_webui.retrieval.vector.async_client import ASYNC_VECTOR_DB_CLIENT
|
||||
from open_webui.routers.images import (
|
||||
CreateImageForm,
|
||||
@@ -2610,6 +2610,7 @@ async def query_chat_files(
|
||||
for idx, doc in enumerate(documents):
|
||||
metadata = metadatas[idx] if idx < len(metadatas) and isinstance(metadatas[idx], dict) else {}
|
||||
chunk = {
|
||||
**filter_source_metadata(metadata),
|
||||
'content': doc,
|
||||
'source': metadata.get('source', metadata.get('name', source_info.get('name', 'Unknown'))),
|
||||
'file_id': metadata.get('file_id', source_info.get('id', '')),
|
||||
@@ -3322,6 +3323,7 @@ async def query_knowledge_files(
|
||||
|
||||
for idx, doc in enumerate(documents):
|
||||
chunk_info = {
|
||||
**filter_source_metadata(metadatas[idx]),
|
||||
'content': doc,
|
||||
'source': metadatas[idx].get('source', metadatas[idx].get('name', 'Unknown')),
|
||||
'file_id': metadatas[idx].get('file_id', ''),
|
||||
@@ -3345,6 +3347,7 @@ async def query_knowledge_files(
|
||||
for idx, doc in enumerate(documents):
|
||||
metadata = metadatas[idx] if idx < len(metadatas) else {}
|
||||
chunk_info = {
|
||||
**filter_source_metadata(metadata),
|
||||
'content': doc,
|
||||
'source': metadata.get('source', metadata.get('name', knowledge.name)),
|
||||
'file_id': metadata.get('file_id', f'external-{knowledge.id}'),
|
||||
|
||||
@@ -2,6 +2,7 @@ import ast
|
||||
import asyncio
|
||||
import base64
|
||||
import copy
|
||||
import html
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
@@ -52,7 +53,7 @@ from open_webui.models.models import Models
|
||||
from open_webui.models.notes import Notes
|
||||
from open_webui.models.oauth_sessions import OAuthSessions
|
||||
from open_webui.models.users import UserModel, Users
|
||||
from open_webui.retrieval.utils import get_sources_from_items
|
||||
from open_webui.retrieval.utils import filter_source_metadata, get_sources_from_items
|
||||
from open_webui.routers.images import (
|
||||
CreateImageForm,
|
||||
EditImageForm,
|
||||
@@ -952,11 +953,17 @@ def get_source_context(sources: list, source_ids: dict = None, include_content:
|
||||
src_type = source.get('source', {}).get('type')
|
||||
src_rid = source.get('source', {}).get('id')
|
||||
body = doc if include_content else ''
|
||||
extra_attrs = ''
|
||||
for key, value in filter_source_metadata(meta).items():
|
||||
if key in ('id', 'name', 'resource-type', 'resource-id'):
|
||||
continue
|
||||
extra_attrs += f' {key}="{html.escape(str(value))}"'
|
||||
context_string += (
|
||||
f'<source id="{source_ids[source_id]}"'
|
||||
+ (f' name="{src_name}"' if src_name else '')
|
||||
+ (f' resource-type="{src_type}"' if src_type else '')
|
||||
+ (f' resource-id="{src_rid}"' if src_rid else '')
|
||||
+ extra_attrs
|
||||
+ f'>{body}</source>\n'
|
||||
)
|
||||
return context_string
|
||||
|
||||
Reference in New Issue
Block a user