Moved branding files to OpenWebUI's new static assets location
@@ -587,6 +587,17 @@ load_oauth_providers()
|
||||
|
||||
STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static")).resolve()
|
||||
|
||||
for file_path in (FRONTEND_BUILD_DIR / "static").glob("**/*"):
|
||||
if file_path.is_file():
|
||||
target_path = STATIC_DIR / file_path.relative_to(
|
||||
(FRONTEND_BUILD_DIR / "static")
|
||||
)
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
shutil.copyfile(file_path, target_path)
|
||||
except Exception as e:
|
||||
logging.error(f"An error occurred: {e}")
|
||||
|
||||
frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png"
|
||||
|
||||
if frontend_favicon.exists():
|
||||
@@ -659,11 +670,7 @@ if CUSTOM_NAME:
|
||||
# LICENSE_KEY
|
||||
####################################
|
||||
|
||||
LICENSE_KEY = PersistentConfig(
|
||||
"LICENSE_KEY",
|
||||
"license.key",
|
||||
os.environ.get("LICENSE_KEY", ""),
|
||||
)
|
||||
LICENSE_KEY = os.environ.get("LICENSE_KEY", "")
|
||||
|
||||
####################################
|
||||
# STORAGE PROVIDER
|
||||
@@ -695,16 +702,16 @@ AZURE_STORAGE_KEY = os.environ.get("AZURE_STORAGE_KEY", None)
|
||||
# File Upload DIR
|
||||
####################################
|
||||
|
||||
UPLOAD_DIR = f"{DATA_DIR}/uploads"
|
||||
Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True)
|
||||
UPLOAD_DIR = DATA_DIR / "uploads"
|
||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
####################################
|
||||
# Cache DIR
|
||||
####################################
|
||||
|
||||
CACHE_DIR = f"{DATA_DIR}/cache"
|
||||
Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
|
||||
CACHE_DIR = DATA_DIR / "cache"
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
####################################
|
||||
@@ -1373,6 +1380,11 @@ Responses from models: {{responses}}"""
|
||||
# Code Interpreter
|
||||
####################################
|
||||
|
||||
ENABLE_CODE_EXECUTION = PersistentConfig(
|
||||
"ENABLE_CODE_EXECUTION",
|
||||
"code_execution.enable",
|
||||
os.environ.get("ENABLE_CODE_EXECUTION", "True").lower() == "true",
|
||||
)
|
||||
|
||||
CODE_EXECUTION_ENGINE = PersistentConfig(
|
||||
"CODE_EXECUTION_ENGINE",
|
||||
@@ -1541,6 +1553,17 @@ OPENSEARCH_CERT_VERIFY = os.environ.get("OPENSEARCH_CERT_VERIFY", False)
|
||||
OPENSEARCH_USERNAME = os.environ.get("OPENSEARCH_USERNAME", None)
|
||||
OPENSEARCH_PASSWORD = os.environ.get("OPENSEARCH_PASSWORD", None)
|
||||
|
||||
# ElasticSearch
|
||||
ELASTICSEARCH_URL = os.environ.get("ELASTICSEARCH_URL", "https://localhost:9200")
|
||||
ELASTICSEARCH_CA_CERTS = os.environ.get("ELASTICSEARCH_CA_CERTS", None)
|
||||
ELASTICSEARCH_API_KEY = os.environ.get("ELASTICSEARCH_API_KEY", None)
|
||||
ELASTICSEARCH_USERNAME = os.environ.get("ELASTICSEARCH_USERNAME", None)
|
||||
ELASTICSEARCH_PASSWORD = os.environ.get("ELASTICSEARCH_PASSWORD", None)
|
||||
ELASTICSEARCH_CLOUD_ID = os.environ.get("ELASTICSEARCH_CLOUD_ID", None)
|
||||
SSL_ASSERT_FINGERPRINT = os.environ.get("SSL_ASSERT_FINGERPRINT", None)
|
||||
ELASTICSEARCH_INDEX_PREFIX = os.environ.get(
|
||||
"ELASTICSEARCH_INDEX_PREFIX", "open_webui_collections"
|
||||
)
|
||||
# Pgvector
|
||||
PGVECTOR_DB_URL = os.environ.get("PGVECTOR_DB_URL", DATABASE_URL)
|
||||
if VECTOR_DB == "pgvector" and not PGVECTOR_DB_URL.startswith("postgres"):
|
||||
@@ -1977,6 +2000,12 @@ EXA_API_KEY = PersistentConfig(
|
||||
os.getenv("EXA_API_KEY", ""),
|
||||
)
|
||||
|
||||
PERPLEXITY_API_KEY = PersistentConfig(
|
||||
"PERPLEXITY_API_KEY",
|
||||
"rag.web.search.perplexity_api_key",
|
||||
os.getenv("PERPLEXITY_API_KEY", ""),
|
||||
)
|
||||
|
||||
RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig(
|
||||
"RAG_WEB_SEARCH_RESULT_COUNT",
|
||||
"rag.web.search.result_count",
|
||||
|
||||
@@ -65,10 +65,8 @@ except Exception:
|
||||
# LOGGING
|
||||
####################################
|
||||
|
||||
log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
|
||||
|
||||
GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
|
||||
if GLOBAL_LOG_LEVEL in log_levels:
|
||||
if GLOBAL_LOG_LEVEL in logging.getLevelNamesMapping():
|
||||
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
|
||||
else:
|
||||
GLOBAL_LOG_LEVEL = "INFO"
|
||||
@@ -78,6 +76,7 @@ log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
|
||||
|
||||
if "cuda_error" in locals():
|
||||
log.exception(cuda_error)
|
||||
del cuda_error
|
||||
|
||||
log_sources = [
|
||||
"AUDIO",
|
||||
@@ -100,7 +99,7 @@ SRC_LOG_LEVELS = {}
|
||||
for source in log_sources:
|
||||
log_env_var = source + "_LOG_LEVEL"
|
||||
SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper()
|
||||
if SRC_LOG_LEVELS[source] not in log_levels:
|
||||
if SRC_LOG_LEVELS[source] not in logging.getLevelNamesMapping():
|
||||
SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL
|
||||
log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}")
|
||||
|
||||
@@ -386,6 +385,7 @@ ENABLE_WEBSOCKET_SUPPORT = (
|
||||
WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
|
||||
|
||||
WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", REDIS_URL)
|
||||
WEBSOCKET_REDIS_LOCK_TIMEOUT = os.environ.get("WEBSOCKET_REDIS_LOCK_TIMEOUT", 60)
|
||||
|
||||
AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "")
|
||||
|
||||
@@ -397,19 +397,20 @@ else:
|
||||
except Exception:
|
||||
AIOHTTP_CLIENT_TIMEOUT = 300
|
||||
|
||||
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = os.environ.get(
|
||||
"AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", ""
|
||||
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = os.environ.get(
|
||||
"AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST",
|
||||
os.environ.get("AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", ""),
|
||||
)
|
||||
|
||||
if AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST == "":
|
||||
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = None
|
||||
|
||||
if AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST == "":
|
||||
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = None
|
||||
else:
|
||||
try:
|
||||
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = int(
|
||||
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST
|
||||
)
|
||||
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = int(AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
|
||||
except Exception:
|
||||
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = 5
|
||||
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = 5
|
||||
|
||||
|
||||
####################################
|
||||
# OFFLINE_MODE
|
||||
|
||||
@@ -105,6 +105,7 @@ from open_webui.config import (
|
||||
# Direct Connections
|
||||
ENABLE_DIRECT_CONNECTIONS,
|
||||
# Code Execution
|
||||
ENABLE_CODE_EXECUTION,
|
||||
CODE_EXECUTION_ENGINE,
|
||||
CODE_EXECUTION_JUPYTER_URL,
|
||||
CODE_EXECUTION_JUPYTER_AUTH,
|
||||
@@ -215,6 +216,7 @@ from open_webui.config import (
|
||||
BING_SEARCH_V7_SUBSCRIPTION_KEY,
|
||||
BRAVE_SEARCH_API_KEY,
|
||||
EXA_API_KEY,
|
||||
PERPLEXITY_API_KEY,
|
||||
KAGI_SEARCH_API_KEY,
|
||||
MOJEEK_SEARCH_API_KEY,
|
||||
BOCHA_SEARCH_API_KEY,
|
||||
@@ -400,8 +402,8 @@ async def lifespan(app: FastAPI):
|
||||
if RESET_CONFIG_ON_START:
|
||||
reset_config()
|
||||
|
||||
if app.state.config.LICENSE_KEY:
|
||||
get_license_data(app, app.state.config.LICENSE_KEY)
|
||||
if LICENSE_KEY:
|
||||
get_license_data(app, LICENSE_KEY)
|
||||
|
||||
asyncio.create_task(periodic_usage_pool_cleanup())
|
||||
yield
|
||||
@@ -419,7 +421,7 @@ oauth_manager = OAuthManager(app)
|
||||
app.state.config = AppConfig()
|
||||
|
||||
app.state.WEBUI_NAME = WEBUI_NAME
|
||||
app.state.config.LICENSE_KEY = LICENSE_KEY
|
||||
app.state.LICENSE_METADATA = None
|
||||
|
||||
########################################
|
||||
#
|
||||
@@ -603,6 +605,7 @@ app.state.config.JINA_API_KEY = JINA_API_KEY
|
||||
app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT
|
||||
app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY
|
||||
app.state.config.EXA_API_KEY = EXA_API_KEY
|
||||
app.state.config.PERPLEXITY_API_KEY = PERPLEXITY_API_KEY
|
||||
|
||||
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT
|
||||
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
|
||||
@@ -658,6 +661,7 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function(
|
||||
#
|
||||
########################################
|
||||
|
||||
app.state.config.ENABLE_CODE_EXECUTION = ENABLE_CODE_EXECUTION
|
||||
app.state.config.CODE_EXECUTION_ENGINE = CODE_EXECUTION_ENGINE
|
||||
app.state.config.CODE_EXECUTION_JUPYTER_URL = CODE_EXECUTION_JUPYTER_URL
|
||||
app.state.config.CODE_EXECUTION_JUPYTER_AUTH = CODE_EXECUTION_JUPYTER_AUTH
|
||||
@@ -1019,7 +1023,7 @@ async def chat_completion(
|
||||
"files": form_data.get("files", None),
|
||||
"features": form_data.get("features", None),
|
||||
"variables": form_data.get("variables", None),
|
||||
"model": model_info.model_dump() if model_info else model,
|
||||
"model": model,
|
||||
"direct": model_item.get("direct", False),
|
||||
**(
|
||||
{"function_calling": "native"}
|
||||
@@ -1037,7 +1041,7 @@ async def chat_completion(
|
||||
form_data["metadata"] = metadata
|
||||
|
||||
form_data, metadata, events = await process_chat_payload(
|
||||
request, form_data, metadata, user, model
|
||||
request, form_data, user, metadata, model
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -1051,7 +1055,7 @@ async def chat_completion(
|
||||
response = await chat_completion_handler(request, form_data, user)
|
||||
|
||||
return await process_chat_response(
|
||||
request, response, form_data, user, events, metadata, tasks
|
||||
request, response, form_data, user, metadata, model, events, tasks
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
@@ -1140,9 +1144,10 @@ async def get_app_config(request: Request):
|
||||
if data is not None and "id" in data:
|
||||
user = Users.get_user_by_id(data["id"])
|
||||
|
||||
user_count = Users.get_num_users()
|
||||
onboarding = False
|
||||
|
||||
if user is None:
|
||||
user_count = Users.get_num_users()
|
||||
onboarding = user_count == 0
|
||||
|
||||
return {
|
||||
@@ -1170,6 +1175,7 @@ async def get_app_config(request: Request):
|
||||
"enable_direct_connections": app.state.config.ENABLE_DIRECT_CONNECTIONS,
|
||||
"enable_channels": app.state.config.ENABLE_CHANNELS,
|
||||
"enable_web_search": app.state.config.ENABLE_RAG_WEB_SEARCH,
|
||||
"enable_code_execution": app.state.config.ENABLE_CODE_EXECUTION,
|
||||
"enable_code_interpreter": app.state.config.ENABLE_CODE_INTERPRETER,
|
||||
"enable_image_generation": app.state.config.ENABLE_IMAGE_GENERATION,
|
||||
"enable_autocomplete_generation": app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
|
||||
@@ -1188,6 +1194,7 @@ async def get_app_config(request: Request):
|
||||
{
|
||||
"default_models": app.state.config.DEFAULT_MODELS,
|
||||
"default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
|
||||
"user_count": user_count,
|
||||
"code": {
|
||||
"engine": app.state.config.CODE_EXECUTION_ENGINE,
|
||||
},
|
||||
@@ -1211,6 +1218,14 @@ async def get_app_config(request: Request):
|
||||
"api_key": GOOGLE_DRIVE_API_KEY.value,
|
||||
},
|
||||
"onedrive": {"client_id": ONEDRIVE_CLIENT_ID.value},
|
||||
"license_metadata": app.state.LICENSE_METADATA,
|
||||
**(
|
||||
{
|
||||
"active_entries": app.state.USER_COUNT,
|
||||
}
|
||||
if user.role == "admin"
|
||||
else {}
|
||||
),
|
||||
}
|
||||
if user is not None
|
||||
else {}
|
||||
|
||||
@@ -414,6 +414,13 @@ def get_sources_from_files(
|
||||
]
|
||||
],
|
||||
}
|
||||
elif file.get("file").get("data"):
|
||||
context = {
|
||||
"documents": [[file.get("file").get("data", {}).get("content")]],
|
||||
"metadatas": [
|
||||
[file.get("file").get("data", {}).get("metadata", {})]
|
||||
],
|
||||
}
|
||||
else:
|
||||
collection_names = []
|
||||
if file.get("type") == "collection":
|
||||
|
||||
@@ -16,6 +16,10 @@ elif VECTOR_DB == "pgvector":
|
||||
from open_webui.retrieval.vector.dbs.pgvector import PgvectorClient
|
||||
|
||||
VECTOR_DB_CLIENT = PgvectorClient()
|
||||
elif VECTOR_DB == "elasticsearch":
|
||||
from open_webui.retrieval.vector.dbs.elasticsearch import ElasticsearchClient
|
||||
|
||||
VECTOR_DB_CLIENT = ElasticsearchClient()
|
||||
else:
|
||||
from open_webui.retrieval.vector.dbs.chroma import ChromaClient
|
||||
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
from elasticsearch import Elasticsearch, BadRequestError
|
||||
from typing import Optional
|
||||
import ssl
|
||||
from elasticsearch.helpers import bulk, scan
|
||||
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
|
||||
from open_webui.config import (
|
||||
ELASTICSEARCH_URL,
|
||||
ELASTICSEARCH_CA_CERTS,
|
||||
ELASTICSEARCH_API_KEY,
|
||||
ELASTICSEARCH_USERNAME,
|
||||
ELASTICSEARCH_PASSWORD,
|
||||
ELASTICSEARCH_CLOUD_ID,
|
||||
ELASTICSEARCH_INDEX_PREFIX,
|
||||
SSL_ASSERT_FINGERPRINT,
|
||||
)
|
||||
|
||||
|
||||
class ElasticsearchClient:
|
||||
"""
|
||||
Important:
|
||||
in order to reduce the number of indexes and since the embedding vector length is fixed, we avoid creating
|
||||
an index for each file but store it as a text field, while seperating to different index
|
||||
baesd on the embedding length.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.index_prefix = ELASTICSEARCH_INDEX_PREFIX
|
||||
self.client = Elasticsearch(
|
||||
hosts=[ELASTICSEARCH_URL],
|
||||
ca_certs=ELASTICSEARCH_CA_CERTS,
|
||||
api_key=ELASTICSEARCH_API_KEY,
|
||||
cloud_id=ELASTICSEARCH_CLOUD_ID,
|
||||
basic_auth=(
|
||||
(ELASTICSEARCH_USERNAME, ELASTICSEARCH_PASSWORD)
|
||||
if ELASTICSEARCH_USERNAME and ELASTICSEARCH_PASSWORD
|
||||
else None
|
||||
),
|
||||
ssl_assert_fingerprint=SSL_ASSERT_FINGERPRINT,
|
||||
)
|
||||
|
||||
# Status: works
|
||||
def _get_index_name(self, dimension: int) -> str:
|
||||
return f"{self.index_prefix}_d{str(dimension)}"
|
||||
|
||||
# Status: works
|
||||
def _scan_result_to_get_result(self, result) -> GetResult:
|
||||
if not result:
|
||||
return None
|
||||
ids = []
|
||||
documents = []
|
||||
metadatas = []
|
||||
|
||||
for hit in result:
|
||||
ids.append(hit["_id"])
|
||||
documents.append(hit["_source"].get("text"))
|
||||
metadatas.append(hit["_source"].get("metadata"))
|
||||
|
||||
return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas])
|
||||
|
||||
# Status: works
|
||||
def _result_to_get_result(self, result) -> GetResult:
|
||||
if not result["hits"]["hits"]:
|
||||
return None
|
||||
ids = []
|
||||
documents = []
|
||||
metadatas = []
|
||||
|
||||
for hit in result["hits"]["hits"]:
|
||||
ids.append(hit["_id"])
|
||||
documents.append(hit["_source"].get("text"))
|
||||
metadatas.append(hit["_source"].get("metadata"))
|
||||
|
||||
return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas])
|
||||
|
||||
# Status: works
|
||||
def _result_to_search_result(self, result) -> SearchResult:
|
||||
ids = []
|
||||
distances = []
|
||||
documents = []
|
||||
metadatas = []
|
||||
|
||||
for hit in result["hits"]["hits"]:
|
||||
ids.append(hit["_id"])
|
||||
distances.append(hit["_score"])
|
||||
documents.append(hit["_source"].get("text"))
|
||||
metadatas.append(hit["_source"].get("metadata"))
|
||||
|
||||
return SearchResult(
|
||||
ids=[ids],
|
||||
distances=[distances],
|
||||
documents=[documents],
|
||||
metadatas=[metadatas],
|
||||
)
|
||||
|
||||
# Status: works
|
||||
def _create_index(self, dimension: int):
|
||||
body = {
|
||||
"mappings": {
|
||||
"dynamic_templates": [
|
||||
{
|
||||
"strings": {
|
||||
"match_mapping_type": "string",
|
||||
"mapping": {"type": "keyword"},
|
||||
}
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"collection": {"type": "keyword"},
|
||||
"id": {"type": "keyword"},
|
||||
"vector": {
|
||||
"type": "dense_vector",
|
||||
"dims": dimension, # Adjust based on your vector dimensions
|
||||
"index": True,
|
||||
"similarity": "cosine",
|
||||
},
|
||||
"text": {"type": "text"},
|
||||
"metadata": {"type": "object"},
|
||||
},
|
||||
}
|
||||
}
|
||||
self.client.indices.create(index=self._get_index_name(dimension), body=body)
|
||||
|
||||
# Status: works
|
||||
|
||||
def _create_batches(self, items: list[VectorItem], batch_size=100):
|
||||
for i in range(0, len(items), batch_size):
|
||||
yield items[i : min(i + batch_size, len(items))]
|
||||
|
||||
# Status: works
|
||||
def has_collection(self, collection_name) -> bool:
|
||||
query_body = {"query": {"bool": {"filter": []}}}
|
||||
query_body["query"]["bool"]["filter"].append(
|
||||
{"term": {"collection": collection_name}}
|
||||
)
|
||||
|
||||
try:
|
||||
result = self.client.count(index=f"{self.index_prefix}*", body=query_body)
|
||||
|
||||
return result.body["count"] > 0
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
def delete_collection(self, collection_name: str):
|
||||
query = {"query": {"term": {"collection": collection_name}}}
|
||||
self.client.delete_by_query(index=f"{self.index_prefix}*", body=query)
|
||||
|
||||
# Status: works
|
||||
def search(
|
||||
self, collection_name: str, vectors: list[list[float]], limit: int
|
||||
) -> Optional[SearchResult]:
|
||||
query = {
|
||||
"size": limit,
|
||||
"_source": ["text", "metadata"],
|
||||
"query": {
|
||||
"script_score": {
|
||||
"query": {
|
||||
"bool": {"filter": [{"term": {"collection": collection_name}}]}
|
||||
},
|
||||
"script": {
|
||||
"source": "cosineSimilarity(params.vector, 'vector') + 1.0",
|
||||
"params": {
|
||||
"vector": vectors[0]
|
||||
}, # Assuming single query vector
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
result = self.client.search(
|
||||
index=self._get_index_name(len(vectors[0])), body=query
|
||||
)
|
||||
|
||||
return self._result_to_search_result(result)
|
||||
|
||||
# Status: only tested halfwat
|
||||
def query(
|
||||
self, collection_name: str, filter: dict, limit: Optional[int] = None
|
||||
) -> Optional[GetResult]:
|
||||
if not self.has_collection(collection_name):
|
||||
return None
|
||||
|
||||
query_body = {
|
||||
"query": {"bool": {"filter": []}},
|
||||
"_source": ["text", "metadata"],
|
||||
}
|
||||
|
||||
for field, value in filter.items():
|
||||
query_body["query"]["bool"]["filter"].append({"term": {field: value}})
|
||||
query_body["query"]["bool"]["filter"].append(
|
||||
{"term": {"collection": collection_name}}
|
||||
)
|
||||
size = limit if limit else 10
|
||||
|
||||
try:
|
||||
result = self.client.search(
|
||||
index=f"{self.index_prefix}*",
|
||||
body=query_body,
|
||||
size=size,
|
||||
)
|
||||
|
||||
return self._result_to_get_result(result)
|
||||
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
# Status: works
|
||||
def _has_index(self, dimension: int):
|
||||
return self.client.indices.exists(
|
||||
index=self._get_index_name(dimension=dimension)
|
||||
)
|
||||
|
||||
def get_or_create_index(self, dimension: int):
|
||||
if not self._has_index(dimension=dimension):
|
||||
self._create_index(dimension=dimension)
|
||||
|
||||
# Status: works
|
||||
def get(self, collection_name: str) -> Optional[GetResult]:
|
||||
# Get all the items in the collection.
|
||||
query = {
|
||||
"query": {"bool": {"filter": [{"term": {"collection": collection_name}}]}},
|
||||
"_source": ["text", "metadata"],
|
||||
}
|
||||
results = list(scan(self.client, index=f"{self.index_prefix}*", query=query))
|
||||
|
||||
return self._scan_result_to_get_result(results)
|
||||
|
||||
# Status: works
|
||||
def insert(self, collection_name: str, items: list[VectorItem]):
|
||||
if not self._has_index(dimension=len(items[0]["vector"])):
|
||||
self._create_index(dimension=len(items[0]["vector"]))
|
||||
|
||||
for batch in self._create_batches(items):
|
||||
actions = [
|
||||
{
|
||||
"_index": self._get_index_name(dimension=len(items[0]["vector"])),
|
||||
"_id": item["id"],
|
||||
"_source": {
|
||||
"collection": collection_name,
|
||||
"vector": item["vector"],
|
||||
"text": item["text"],
|
||||
"metadata": item["metadata"],
|
||||
},
|
||||
}
|
||||
for item in batch
|
||||
]
|
||||
bulk(self.client, actions)
|
||||
|
||||
# Upsert documents using the update API with doc_as_upsert=True.
|
||||
def upsert(self, collection_name: str, items: list[VectorItem]):
|
||||
if not self._has_index(dimension=len(items[0]["vector"])):
|
||||
self._create_index(dimension=len(items[0]["vector"]))
|
||||
for batch in self._create_batches(items):
|
||||
actions = [
|
||||
{
|
||||
"_op_type": "update",
|
||||
"_index": self._get_index_name(dimension=len(item["vector"])),
|
||||
"_id": item["id"],
|
||||
"doc": {
|
||||
"collection": collection_name,
|
||||
"vector": item["vector"],
|
||||
"text": item["text"],
|
||||
"metadata": item["metadata"],
|
||||
},
|
||||
"doc_as_upsert": True,
|
||||
}
|
||||
for item in batch
|
||||
]
|
||||
bulk(self.client, actions)
|
||||
|
||||
# Delete specific documents from a collection by filtering on both collection and document IDs.
|
||||
def delete(
|
||||
self,
|
||||
collection_name: str,
|
||||
ids: Optional[list[str]] = None,
|
||||
filter: Optional[dict] = None,
|
||||
):
|
||||
|
||||
query = {
|
||||
"query": {"bool": {"filter": [{"term": {"collection": collection_name}}]}}
|
||||
}
|
||||
# logic based on chromaDB
|
||||
if ids:
|
||||
query["query"]["bool"]["filter"].append({"terms": {"_id": ids}})
|
||||
elif filter:
|
||||
for field, value in filter.items():
|
||||
query["query"]["bool"]["filter"].append(
|
||||
{"term": {f"metadata.{field}": value}}
|
||||
)
|
||||
|
||||
self.client.delete_by_query(index=f"{self.index_prefix}*", body=query)
|
||||
|
||||
def reset(self):
|
||||
indices = self.client.indices.get(index=f"{self.index_prefix}*")
|
||||
for index in indices:
|
||||
self.client.indices.delete(index=index)
|
||||
@@ -20,9 +20,9 @@ class MilvusClient:
|
||||
def __init__(self):
|
||||
self.collection_prefix = "open_webui"
|
||||
if MILVUS_TOKEN is None:
|
||||
self.client = Client(uri=MILVUS_URI, database=MILVUS_DB)
|
||||
self.client = Client(uri=MILVUS_URI, db_name=MILVUS_DB)
|
||||
else:
|
||||
self.client = Client(uri=MILVUS_URI, database=MILVUS_DB, token=MILVUS_TOKEN)
|
||||
self.client = Client(uri=MILVUS_URI, db_name=MILVUS_DB, token=MILVUS_TOKEN)
|
||||
|
||||
def _result_to_get_result(self, result) -> GetResult:
|
||||
ids = []
|
||||
|
||||
@@ -49,7 +49,7 @@ class OpenSearchClient:
|
||||
ids=ids, distances=distances, documents=documents, metadatas=metadatas
|
||||
)
|
||||
|
||||
def _create_index(self, index_name: str, dimension: int):
|
||||
def _create_index(self, collection_name: str, dimension: int):
|
||||
body = {
|
||||
"mappings": {
|
||||
"properties": {
|
||||
@@ -72,24 +72,28 @@ class OpenSearchClient:
|
||||
}
|
||||
}
|
||||
}
|
||||
self.client.indices.create(index=f"{self.index_prefix}_{index_name}", body=body)
|
||||
self.client.indices.create(
|
||||
index=f"{self.index_prefix}_{collection_name}", body=body
|
||||
)
|
||||
|
||||
def _create_batches(self, items: list[VectorItem], batch_size=100):
|
||||
for i in range(0, len(items), batch_size):
|
||||
yield items[i : i + batch_size]
|
||||
|
||||
def has_collection(self, index_name: str) -> bool:
|
||||
def has_collection(self, collection_name: str) -> bool:
|
||||
# has_collection here means has index.
|
||||
# We are simply adapting to the norms of the other DBs.
|
||||
return self.client.indices.exists(index=f"{self.index_prefix}_{index_name}")
|
||||
return self.client.indices.exists(
|
||||
index=f"{self.index_prefix}_{collection_name}"
|
||||
)
|
||||
|
||||
def delete_colleciton(self, index_name: str):
|
||||
def delete_colleciton(self, collection_name: str):
|
||||
# delete_collection here means delete index.
|
||||
# We are simply adapting to the norms of the other DBs.
|
||||
self.client.indices.delete(index=f"{self.index_prefix}_{index_name}")
|
||||
self.client.indices.delete(index=f"{self.index_prefix}_{collection_name}")
|
||||
|
||||
def search(
|
||||
self, index_name: str, vectors: list[list[float]], limit: int
|
||||
self, collection_name: str, vectors: list[list[float]], limit: int
|
||||
) -> Optional[SearchResult]:
|
||||
query = {
|
||||
"size": limit,
|
||||
@@ -108,7 +112,7 @@ class OpenSearchClient:
|
||||
}
|
||||
|
||||
result = self.client.search(
|
||||
index=f"{self.index_prefix}_{index_name}", body=query
|
||||
index=f"{self.index_prefix}_{collection_name}", body=query
|
||||
)
|
||||
|
||||
return self._result_to_search_result(result)
|
||||
@@ -141,21 +145,22 @@ class OpenSearchClient:
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
def get_or_create_index(self, index_name: str, dimension: int):
|
||||
if not self.has_index(index_name):
|
||||
self._create_index(index_name, dimension)
|
||||
def _create_index_if_not_exists(self, collection_name: str, dimension: int):
|
||||
if not self.has_index(collection_name):
|
||||
self._create_index(collection_name, dimension)
|
||||
|
||||
def get(self, index_name: str) -> Optional[GetResult]:
|
||||
def get(self, collection_name: str) -> Optional[GetResult]:
|
||||
query = {"query": {"match_all": {}}, "_source": ["text", "metadata"]}
|
||||
|
||||
result = self.client.search(
|
||||
index=f"{self.index_prefix}_{index_name}", body=query
|
||||
index=f"{self.index_prefix}_{collection_name}", body=query
|
||||
)
|
||||
return self._result_to_get_result(result)
|
||||
|
||||
def insert(self, index_name: str, items: list[VectorItem]):
|
||||
if not self.has_index(index_name):
|
||||
self._create_index(index_name, dimension=len(items[0]["vector"]))
|
||||
def insert(self, collection_name: str, items: list[VectorItem]):
|
||||
self._create_index_if_not_exists(
|
||||
collection_name=collection_name, dimension=len(items[0]["vector"])
|
||||
)
|
||||
|
||||
for batch in self._create_batches(items):
|
||||
actions = [
|
||||
@@ -173,15 +178,17 @@ class OpenSearchClient:
|
||||
]
|
||||
self.client.bulk(actions)
|
||||
|
||||
def upsert(self, index_name: str, items: list[VectorItem]):
|
||||
if not self.has_index(index_name):
|
||||
self._create_index(index_name, dimension=len(items[0]["vector"]))
|
||||
def upsert(self, collection_name: str, items: list[VectorItem]):
|
||||
self._create_index_if_not_exists(
|
||||
collection_name=collection_name, dimension=len(items[0]["vector"])
|
||||
)
|
||||
|
||||
for batch in self._create_batches(items):
|
||||
actions = [
|
||||
{
|
||||
"index": {
|
||||
"_id": item["id"],
|
||||
"_index": f"{self.index_prefix}_{collection_name}",
|
||||
"_source": {
|
||||
"vector": item["vector"],
|
||||
"text": item["text"],
|
||||
@@ -193,9 +200,9 @@ class OpenSearchClient:
|
||||
]
|
||||
self.client.bulk(actions)
|
||||
|
||||
def delete(self, index_name: str, ids: list[str]):
|
||||
def delete(self, collection_name: str, ids: list[str]):
|
||||
actions = [
|
||||
{"delete": {"_index": f"{self.index_prefix}_{index_name}", "_id": id}}
|
||||
{"delete": {"_index": f"{self.index_prefix}_{collection_name}", "_id": id}}
|
||||
for id in ids
|
||||
]
|
||||
self.client.bulk(body=actions)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
import requests
|
||||
|
||||
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
|
||||
def search_perplexity(
|
||||
api_key: str,
|
||||
query: str,
|
||||
count: int,
|
||||
filter_list: Optional[list[str]] = None,
|
||||
) -> list[SearchResult]:
|
||||
"""Search using Perplexity API and return the results as a list of SearchResult objects.
|
||||
|
||||
Args:
|
||||
api_key (str): A Perplexity API key
|
||||
query (str): The query to search for
|
||||
count (int): Maximum number of results to return
|
||||
|
||||
"""
|
||||
|
||||
# Handle PersistentConfig object
|
||||
if hasattr(api_key, "__str__"):
|
||||
api_key = str(api_key)
|
||||
|
||||
try:
|
||||
url = "https://api.perplexity.ai/chat/completions"
|
||||
|
||||
# Create payload for the API call
|
||||
payload = {
|
||||
"model": "sonar",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a search assistant. Provide factual information with citations.",
|
||||
},
|
||||
{"role": "user", "content": query},
|
||||
],
|
||||
"temperature": 0.2, # Lower temperature for more factual responses
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
# Make the API request
|
||||
response = requests.request("POST", url, json=payload, headers=headers)
|
||||
|
||||
# Parse the JSON response
|
||||
json_response = response.json()
|
||||
|
||||
# Extract citations from the response
|
||||
citations = json_response.get("citations", [])
|
||||
|
||||
# Create search results from citations
|
||||
results = []
|
||||
for i, citation in enumerate(citations[:count]):
|
||||
# Extract content from the response to use as snippet
|
||||
content = ""
|
||||
if "choices" in json_response and json_response["choices"]:
|
||||
if i == 0:
|
||||
content = json_response["choices"][0]["message"]["content"]
|
||||
|
||||
result = {"link": citation, "title": f"Source {i+1}", "snippet": content}
|
||||
results.append(result)
|
||||
|
||||
if filter_list:
|
||||
|
||||
results = get_filtered_results(results, filter_list)
|
||||
|
||||
return [
|
||||
SearchResult(
|
||||
link=result["link"], title=result["title"], snippet=result["snippet"]
|
||||
)
|
||||
for result in results[:count]
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error searching with Perplexity API: {e}")
|
||||
return []
|
||||
@@ -54,7 +54,7 @@ MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["AUDIO"])
|
||||
|
||||
SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
|
||||
SPEECH_CACHE_DIR = CACHE_DIR / "audio" / "speech"
|
||||
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
|
||||
@@ -230,9 +230,12 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
||||
|
||||
entry = connection_app.entries[0]
|
||||
username = str(entry[f"{LDAP_ATTRIBUTE_FOR_USERNAME}"]).lower()
|
||||
mail = str(entry[f"{LDAP_ATTRIBUTE_FOR_MAIL}"])
|
||||
if not mail or mail == "" or mail == "[]":
|
||||
raise HTTPException(400, f"User {form_data.user} does not have mail.")
|
||||
email = str(entry[f"{LDAP_ATTRIBUTE_FOR_MAIL}"])
|
||||
if not email or email == "" or email == "[]":
|
||||
raise HTTPException(400, f"User {form_data.user} does not have email.")
|
||||
else:
|
||||
email = email.lower()
|
||||
|
||||
cn = str(entry["cn"])
|
||||
user_dn = entry.entry_dn
|
||||
|
||||
@@ -247,7 +250,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
||||
if not connection_user.bind():
|
||||
raise HTTPException(400, f"Authentication failed for {form_data.user}")
|
||||
|
||||
user = Users.get_user_by_email(mail)
|
||||
user = Users.get_user_by_email(email)
|
||||
if not user:
|
||||
try:
|
||||
user_count = Users.get_num_users()
|
||||
@@ -259,7 +262,10 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
||||
)
|
||||
|
||||
user = Auths.insert_new_auth(
|
||||
email=mail, password=str(uuid.uuid4()), name=cn, role=role
|
||||
email=email,
|
||||
password=str(uuid.uuid4()),
|
||||
name=cn,
|
||||
role=role,
|
||||
)
|
||||
|
||||
if not user:
|
||||
@@ -272,7 +278,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
||||
except Exception as err:
|
||||
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
||||
|
||||
user = Auths.authenticate_user_by_trusted_header(mail)
|
||||
user = Auths.authenticate_user_by_trusted_header(email)
|
||||
|
||||
if user:
|
||||
token = create_token(
|
||||
|
||||
@@ -70,6 +70,7 @@ async def set_direct_connections_config(
|
||||
# CodeInterpreterConfig
|
||||
############################
|
||||
class CodeInterpreterConfigForm(BaseModel):
|
||||
ENABLE_CODE_EXECUTION: bool
|
||||
CODE_EXECUTION_ENGINE: str
|
||||
CODE_EXECUTION_JUPYTER_URL: Optional[str]
|
||||
CODE_EXECUTION_JUPYTER_AUTH: Optional[str]
|
||||
@@ -89,6 +90,7 @@ class CodeInterpreterConfigForm(BaseModel):
|
||||
@router.get("/code_execution", response_model=CodeInterpreterConfigForm)
|
||||
async def get_code_execution_config(request: Request, user=Depends(get_admin_user)):
|
||||
return {
|
||||
"ENABLE_CODE_EXECUTION": request.app.state.config.ENABLE_CODE_EXECUTION,
|
||||
"CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
|
||||
"CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
|
||||
"CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
|
||||
@@ -111,6 +113,8 @@ async def set_code_execution_config(
|
||||
request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user)
|
||||
):
|
||||
|
||||
request.app.state.config.ENABLE_CODE_EXECUTION = form_data.ENABLE_CODE_EXECUTION
|
||||
|
||||
request.app.state.config.CODE_EXECUTION_ENGINE = form_data.CODE_EXECUTION_ENGINE
|
||||
request.app.state.config.CODE_EXECUTION_JUPYTER_URL = (
|
||||
form_data.CODE_EXECUTION_JUPYTER_URL
|
||||
@@ -153,6 +157,7 @@ async def set_code_execution_config(
|
||||
)
|
||||
|
||||
return {
|
||||
"ENABLE_CODE_EXECUTION": request.app.state.config.ENABLE_CODE_EXECUTION,
|
||||
"CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
|
||||
"CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
|
||||
"CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
|
||||
|
||||
@@ -74,7 +74,7 @@ async def create_new_function(
|
||||
|
||||
function = Functions.insert_new_function(user.id, function_type, form_data)
|
||||
|
||||
function_cache_dir = Path(CACHE_DIR) / "functions" / form_data.id
|
||||
function_cache_dir = CACHE_DIR / "functions" / form_data.id
|
||||
function_cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if function:
|
||||
|
||||
@@ -25,7 +25,7 @@ from pydantic import BaseModel
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["IMAGES"])
|
||||
|
||||
IMAGE_CACHE_DIR = Path(CACHE_DIR).joinpath("./image/generations/")
|
||||
IMAGE_CACHE_DIR = CACHE_DIR / "image" / "generations"
|
||||
IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
@@ -517,7 +517,13 @@ async def image_generations(
|
||||
images = []
|
||||
|
||||
for image in res["data"]:
|
||||
image_data, content_type = load_b64_image_data(image["b64_json"])
|
||||
if "url" in image:
|
||||
image_data, content_type = load_url_image_data(
|
||||
image["url"], headers
|
||||
)
|
||||
else:
|
||||
image_data, content_type = load_b64_image_data(image["b64_json"])
|
||||
|
||||
url = upload_image(request, data, image_data, content_type, user)
|
||||
images.append({"url": url})
|
||||
return images
|
||||
|
||||
@@ -55,7 +55,7 @@ from open_webui.env import (
|
||||
ENV,
|
||||
SRC_LOG_LEVELS,
|
||||
AIOHTTP_CLIENT_TIMEOUT,
|
||||
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST,
|
||||
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
|
||||
BYPASS_MODEL_ACCESS_CONTROL,
|
||||
)
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
@@ -72,7 +72,7 @@ log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
|
||||
|
||||
|
||||
async def send_get_request(url, key=None, user: UserModel = None):
|
||||
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
|
||||
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||
async with session.get(
|
||||
@@ -216,7 +216,7 @@ async def verify_connection(
|
||||
key = form_data.key
|
||||
|
||||
async with aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
|
||||
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
|
||||
) as session:
|
||||
try:
|
||||
async with session.get(
|
||||
|
||||
@@ -22,7 +22,7 @@ from open_webui.config import (
|
||||
)
|
||||
from open_webui.env import (
|
||||
AIOHTTP_CLIENT_TIMEOUT,
|
||||
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST,
|
||||
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
|
||||
ENABLE_FORWARD_USER_INFO_HEADERS,
|
||||
BYPASS_MODEL_ACCESS_CONTROL,
|
||||
)
|
||||
@@ -53,7 +53,7 @@ log.setLevel(SRC_LOG_LEVELS["OPENAI"])
|
||||
|
||||
|
||||
async def send_get_request(url, key=None, user: UserModel = None):
|
||||
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
|
||||
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||
async with session.get(
|
||||
@@ -192,7 +192,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||
body = await request.body()
|
||||
name = hashlib.sha256(body).hexdigest()
|
||||
|
||||
SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
|
||||
SPEECH_CACHE_DIR = CACHE_DIR / "audio" / "speech"
|
||||
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
|
||||
file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
|
||||
@@ -448,9 +448,7 @@ async def get_models(
|
||||
|
||||
r = None
|
||||
async with aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(
|
||||
total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST
|
||||
)
|
||||
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
|
||||
) as session:
|
||||
try:
|
||||
async with session.get(
|
||||
@@ -530,7 +528,7 @@ async def verify_connection(
|
||||
key = form_data.key
|
||||
|
||||
async with aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
|
||||
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
|
||||
) as session:
|
||||
try:
|
||||
async with session.get(
|
||||
|
||||
@@ -59,7 +59,7 @@ from open_webui.retrieval.web.serpstack import search_serpstack
|
||||
from open_webui.retrieval.web.tavily import search_tavily
|
||||
from open_webui.retrieval.web.bing import search_bing
|
||||
from open_webui.retrieval.web.exa import search_exa
|
||||
|
||||
from open_webui.retrieval.web.perplexity import search_perplexity
|
||||
|
||||
from open_webui.retrieval.utils import (
|
||||
get_embedding_function,
|
||||
@@ -405,6 +405,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
|
||||
"bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
|
||||
"bing_search_v7_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY,
|
||||
"exa_api_key": request.app.state.config.EXA_API_KEY,
|
||||
"perplexity_api_key": request.app.state.config.PERPLEXITY_API_KEY,
|
||||
"result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
"trust_env": request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV,
|
||||
"concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||
@@ -465,6 +466,7 @@ class WebSearchConfig(BaseModel):
|
||||
bing_search_v7_endpoint: Optional[str] = None
|
||||
bing_search_v7_subscription_key: Optional[str] = None
|
||||
exa_api_key: Optional[str] = None
|
||||
perplexity_api_key: Optional[str] = None
|
||||
result_count: Optional[int] = None
|
||||
concurrent_requests: Optional[int] = None
|
||||
trust_env: Optional[bool] = None
|
||||
@@ -617,6 +619,10 @@ async def update_rag_config(
|
||||
|
||||
request.app.state.config.EXA_API_KEY = form_data.web.search.exa_api_key
|
||||
|
||||
request.app.state.config.PERPLEXITY_API_KEY = (
|
||||
form_data.web.search.perplexity_api_key
|
||||
)
|
||||
|
||||
request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = (
|
||||
form_data.web.search.result_count
|
||||
)
|
||||
@@ -683,6 +689,7 @@ async def update_rag_config(
|
||||
"bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
|
||||
"bing_search_v7_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY,
|
||||
"exa_api_key": request.app.state.config.EXA_API_KEY,
|
||||
"perplexity_api_key": request.app.state.config.PERPLEXITY_API_KEY,
|
||||
"result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
"concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||
"trust_env": request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV,
|
||||
@@ -1182,9 +1189,13 @@ def process_web(
|
||||
content = " ".join([doc.page_content for doc in docs])
|
||||
|
||||
log.debug(f"text_content: {content}")
|
||||
save_docs_to_vector_db(
|
||||
request, docs, collection_name, overwrite=True, user=user
|
||||
)
|
||||
|
||||
if not request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL:
|
||||
save_docs_to_vector_db(
|
||||
request, docs, collection_name, overwrite=True, user=user
|
||||
)
|
||||
else:
|
||||
collection_name = None
|
||||
|
||||
return {
|
||||
"status": True,
|
||||
@@ -1196,6 +1207,7 @@ def process_web(
|
||||
},
|
||||
"meta": {
|
||||
"name": form_data.url,
|
||||
"source": form_data.url,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1221,6 +1233,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
|
||||
- SERPLY_API_KEY
|
||||
- TAVILY_API_KEY
|
||||
- EXA_API_KEY
|
||||
- PERPLEXITY_API_KEY
|
||||
- SEARCHAPI_API_KEY + SEARCHAPI_ENGINE (by default `google`)
|
||||
- SERPAPI_API_KEY + SERPAPI_ENGINE (by default `google`)
|
||||
Args:
|
||||
@@ -1385,6 +1398,13 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
|
||||
request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||
)
|
||||
elif engine == "perplexity":
|
||||
return search_perplexity(
|
||||
request.app.state.config.PERPLEXITY_API_KEY,
|
||||
query,
|
||||
request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||
)
|
||||
else:
|
||||
raise Exception("No search engine API key found in environment variables")
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ async def create_new_tools(
|
||||
specs = get_tools_specs(TOOLS[form_data.id])
|
||||
tools = Tools.insert_new_tool(user.id, form_data, specs)
|
||||
|
||||
tool_cache_dir = Path(CACHE_DIR) / "tools" / form_data.id
|
||||
tool_cache_dir = CACHE_DIR / "tools" / form_data.id
|
||||
tool_cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if tools:
|
||||
|
||||
@@ -12,6 +12,7 @@ from open_webui.env import (
|
||||
ENABLE_WEBSOCKET_SUPPORT,
|
||||
WEBSOCKET_MANAGER,
|
||||
WEBSOCKET_REDIS_URL,
|
||||
WEBSOCKET_REDIS_LOCK_TIMEOUT,
|
||||
)
|
||||
from open_webui.utils.auth import decode_token
|
||||
from open_webui.socket.utils import RedisDict, RedisLock
|
||||
@@ -61,7 +62,7 @@ if WEBSOCKET_MANAGER == "redis":
|
||||
clean_up_lock = RedisLock(
|
||||
redis_url=WEBSOCKET_REDIS_URL,
|
||||
lock_name="usage_cleanup_lock",
|
||||
timeout_secs=TIMEOUT_DURATION * 2,
|
||||
timeout_secs=WEBSOCKET_REDIS_LOCK_TIMEOUT,
|
||||
)
|
||||
aquire_func = clean_up_lock.aquire_lock
|
||||
renew_func = clean_up_lock.renew_lock
|
||||
|
||||
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Open WebUI",
|
||||
"short_name": "WebUI",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/static/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 312 KiB |
@@ -101,19 +101,33 @@ class LocalStorageProvider(StorageProvider):
|
||||
|
||||
class S3StorageProvider(StorageProvider):
|
||||
def __init__(self):
|
||||
self.s3_client = boto3.client(
|
||||
"s3",
|
||||
region_name=S3_REGION_NAME,
|
||||
endpoint_url=S3_ENDPOINT_URL,
|
||||
aws_access_key_id=S3_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=S3_SECRET_ACCESS_KEY,
|
||||
config=Config(
|
||||
s3={
|
||||
"use_accelerate_endpoint": S3_USE_ACCELERATE_ENDPOINT,
|
||||
"addressing_style": S3_ADDRESSING_STYLE,
|
||||
},
|
||||
),
|
||||
config = Config(
|
||||
s3={
|
||||
"use_accelerate_endpoint": S3_USE_ACCELERATE_ENDPOINT,
|
||||
"addressing_style": S3_ADDRESSING_STYLE,
|
||||
},
|
||||
)
|
||||
|
||||
# If access key and secret are provided, use them for authentication
|
||||
if S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY:
|
||||
self.s3_client = boto3.client(
|
||||
"s3",
|
||||
region_name=S3_REGION_NAME,
|
||||
endpoint_url=S3_ENDPOINT_URL,
|
||||
aws_access_key_id=S3_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=S3_SECRET_ACCESS_KEY,
|
||||
config=config,
|
||||
)
|
||||
else:
|
||||
# If no explicit credentials are provided, fall back to default AWS credentials
|
||||
# This supports workload identity (IAM roles for EC2, EKS, etc.)
|
||||
self.s3_client = boto3.client(
|
||||
"s3",
|
||||
region_name=S3_REGION_NAME,
|
||||
endpoint_url=S3_ENDPOINT_URL,
|
||||
config=config,
|
||||
)
|
||||
|
||||
self.bucket_name = S3_BUCKET_NAME
|
||||
self.key_prefix = S3_KEY_PREFIX if S3_KEY_PREFIX else ""
|
||||
|
||||
|
||||
@@ -187,6 +187,17 @@ class TestS3StorageProvider:
|
||||
assert not (upload_dir / self.filename).exists()
|
||||
assert not (upload_dir / self.filename_extra).exists()
|
||||
|
||||
def test_init_without_credentials(self, monkeypatch):
|
||||
"""Test that S3StorageProvider can initialize without explicit credentials."""
|
||||
# Temporarily unset the environment variables
|
||||
monkeypatch.setattr(provider, "S3_ACCESS_KEY_ID", None)
|
||||
monkeypatch.setattr(provider, "S3_SECRET_ACCESS_KEY", None)
|
||||
|
||||
# Should not raise an exception
|
||||
storage = provider.S3StorageProvider()
|
||||
assert storage.s3_client is not None
|
||||
assert storage.bucket_name == provider.S3_BUCKET_NAME
|
||||
|
||||
|
||||
class TestGCSStorageProvider:
|
||||
Storage = provider.GCSStorageProvider()
|
||||
|
||||
@@ -72,7 +72,7 @@ def get_license_data(app, key):
|
||||
if key:
|
||||
try:
|
||||
res = requests.post(
|
||||
"https://api.openwebui.com/api/v1/license",
|
||||
"https://api.openwebui.com/api/v1/license/",
|
||||
json={"key": key, "version": "1"},
|
||||
timeout=5,
|
||||
)
|
||||
@@ -83,11 +83,12 @@ def get_license_data(app, key):
|
||||
if k == "resources":
|
||||
for p, c in v.items():
|
||||
globals().get("override_static", lambda a, b: None)(p, c)
|
||||
elif k == "user_count":
|
||||
elif k == "count":
|
||||
setattr(app.state, "USER_COUNT", v)
|
||||
elif k == "webui_name":
|
||||
elif k == "name":
|
||||
setattr(app.state, "WEBUI_NAME", v)
|
||||
|
||||
elif k == "metadata":
|
||||
setattr(app.state, "LICENSE_METADATA", v)
|
||||
return True
|
||||
else:
|
||||
log.error(
|
||||
|
||||
@@ -149,7 +149,7 @@ async def generate_direct_chat_completion(
|
||||
}
|
||||
)
|
||||
|
||||
if "error" in res:
|
||||
if "error" in res and res["error"]:
|
||||
raise Exception(res["error"])
|
||||
|
||||
return res
|
||||
@@ -328,9 +328,14 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
|
||||
}
|
||||
|
||||
try:
|
||||
filter_functions = [
|
||||
Functions.get_function_by_id(filter_id)
|
||||
for filter_id in get_sorted_filter_ids(model)
|
||||
]
|
||||
|
||||
result, _ = await process_filter_functions(
|
||||
request=request,
|
||||
filter_ids=get_sorted_filter_ids(model),
|
||||
filter_functions=filter_functions,
|
||||
filter_type="outlet",
|
||||
form_data=data,
|
||||
extra_params=extra_params,
|
||||
|
||||
@@ -1,148 +1,210 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
import aiohttp
|
||||
import websockets
|
||||
import requests
|
||||
from urllib.parse import urljoin
|
||||
from pydantic import BaseModel
|
||||
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(SRC_LOG_LEVELS["MAIN"])
|
||||
|
||||
|
||||
async def execute_code_jupyter(
|
||||
jupyter_url, code, token=None, password=None, timeout=10
|
||||
):
|
||||
class ResultModel(BaseModel):
|
||||
"""
|
||||
Executes Python code in a Jupyter kernel.
|
||||
Supports authentication with a token or password.
|
||||
:param jupyter_url: Jupyter server URL (e.g., "http://localhost:8888")
|
||||
:param code: Code to execute
|
||||
:param token: Jupyter authentication token (optional)
|
||||
:param password: Jupyter password (optional)
|
||||
:param timeout: WebSocket timeout in seconds (default: 10s)
|
||||
:return: Dictionary with stdout, stderr, and result
|
||||
- Images are prefixed with "base64:image/png," and separated by newlines if multiple.
|
||||
Execute Code Result Model
|
||||
"""
|
||||
session = requests.Session() # Maintain cookies
|
||||
headers = {} # Headers for requests
|
||||
|
||||
# Authenticate using password
|
||||
if password and not token:
|
||||
stdout: Optional[str] = ""
|
||||
stderr: Optional[str] = ""
|
||||
result: Optional[str] = ""
|
||||
|
||||
|
||||
class JupyterCodeExecuter:
|
||||
"""
|
||||
Execute code in jupyter notebook
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str,
|
||||
code: str,
|
||||
token: str = "",
|
||||
password: str = "",
|
||||
timeout: int = 60,
|
||||
):
|
||||
"""
|
||||
:param base_url: Jupyter server URL (e.g., "http://localhost:8888")
|
||||
:param code: Code to execute
|
||||
:param token: Jupyter authentication token (optional)
|
||||
:param password: Jupyter password (optional)
|
||||
:param timeout: WebSocket timeout in seconds (default: 60s)
|
||||
"""
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.code = code
|
||||
self.token = token
|
||||
self.password = password
|
||||
self.timeout = timeout
|
||||
self.kernel_id = ""
|
||||
self.session = aiohttp.ClientSession(base_url=self.base_url)
|
||||
self.params = {}
|
||||
self.result = ResultModel()
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.kernel_id:
|
||||
try:
|
||||
async with self.session.delete(
|
||||
f"/api/kernels/{self.kernel_id}", params=self.params
|
||||
) as response:
|
||||
response.raise_for_status()
|
||||
except Exception as err:
|
||||
logger.exception("close kernel failed, %s", err)
|
||||
await self.session.close()
|
||||
|
||||
async def run(self) -> ResultModel:
|
||||
try:
|
||||
login_url = urljoin(jupyter_url, "/login")
|
||||
response = session.get(login_url)
|
||||
await self.sign_in()
|
||||
await self.init_kernel()
|
||||
await self.execute_code()
|
||||
except Exception as err:
|
||||
logger.exception("execute code failed, %s", err)
|
||||
self.result.stderr = f"Error: {err}"
|
||||
return self.result
|
||||
|
||||
async def sign_in(self) -> None:
|
||||
# password authentication
|
||||
if self.password and not self.token:
|
||||
async with self.session.get("/login") as response:
|
||||
response.raise_for_status()
|
||||
xsrf_token = response.cookies["_xsrf"].value
|
||||
if not xsrf_token:
|
||||
raise ValueError("_xsrf token not found")
|
||||
self.session.cookie_jar.update_cookies(response.cookies)
|
||||
self.session.headers.update({"X-XSRFToken": xsrf_token})
|
||||
async with self.session.post(
|
||||
"/login",
|
||||
data={"_xsrf": xsrf_token, "password": self.password},
|
||||
allow_redirects=False,
|
||||
) as response:
|
||||
response.raise_for_status()
|
||||
self.session.cookie_jar.update_cookies(response.cookies)
|
||||
|
||||
# token authentication
|
||||
if self.token:
|
||||
self.params.update({"token": self.token})
|
||||
|
||||
async def init_kernel(self) -> None:
|
||||
async with self.session.post(
|
||||
url="/api/kernels", params=self.params
|
||||
) as response:
|
||||
response.raise_for_status()
|
||||
xsrf_token = session.cookies.get("_xsrf")
|
||||
if not xsrf_token:
|
||||
raise ValueError("Failed to fetch _xsrf token")
|
||||
|
||||
login_data = {"_xsrf": xsrf_token, "password": password}
|
||||
login_response = session.post(
|
||||
login_url, data=login_data, cookies=session.cookies
|
||||
)
|
||||
login_response.raise_for_status()
|
||||
headers["X-XSRFToken"] = xsrf_token
|
||||
except Exception as e:
|
||||
return {
|
||||
"stdout": "",
|
||||
"stderr": f"Authentication Error: {str(e)}",
|
||||
"result": "",
|
||||
}
|
||||
|
||||
# Construct API URLs with authentication token if provided
|
||||
params = f"?token={token}" if token else ""
|
||||
kernel_url = urljoin(jupyter_url, f"/api/kernels{params}")
|
||||
|
||||
try:
|
||||
response = session.post(kernel_url, headers=headers, cookies=session.cookies)
|
||||
response.raise_for_status()
|
||||
kernel_id = response.json()["id"]
|
||||
|
||||
websocket_url = urljoin(
|
||||
jupyter_url.replace("http", "ws"),
|
||||
f"/api/kernels/{kernel_id}/channels{params}",
|
||||
)
|
||||
kernel_data = await response.json()
|
||||
self.kernel_id = kernel_data["id"]
|
||||
|
||||
def init_ws(self) -> (str, dict):
|
||||
ws_base = self.base_url.replace("http", "ws")
|
||||
ws_params = "?" + "&".join([f"{key}={val}" for key, val in self.params.items()])
|
||||
websocket_url = f"{ws_base}/api/kernels/{self.kernel_id}/channels{ws_params if len(ws_params) > 1 else ''}"
|
||||
ws_headers = {}
|
||||
if password and not token:
|
||||
ws_headers["X-XSRFToken"] = session.cookies.get("_xsrf")
|
||||
cookies = {name: value for name, value in session.cookies.items()}
|
||||
ws_headers["Cookie"] = "; ".join(
|
||||
[f"{name}={value}" for name, value in cookies.items()]
|
||||
)
|
||||
if self.password and not self.token:
|
||||
ws_headers = {
|
||||
"Cookie": "; ".join(
|
||||
[
|
||||
f"{cookie.key}={cookie.value}"
|
||||
for cookie in self.session.cookie_jar
|
||||
]
|
||||
),
|
||||
**self.session.headers,
|
||||
}
|
||||
return websocket_url, ws_headers
|
||||
|
||||
async def execute_code(self) -> None:
|
||||
# initialize ws
|
||||
websocket_url, ws_headers = self.init_ws()
|
||||
# execute
|
||||
async with websockets.connect(
|
||||
websocket_url, additional_headers=ws_headers
|
||||
) as ws:
|
||||
msg_id = str(uuid.uuid4())
|
||||
execute_request = {
|
||||
"header": {
|
||||
"msg_id": msg_id,
|
||||
"msg_type": "execute_request",
|
||||
"username": "user",
|
||||
"session": str(uuid.uuid4()),
|
||||
"date": "",
|
||||
"version": "5.3",
|
||||
},
|
||||
"parent_header": {},
|
||||
"metadata": {},
|
||||
"content": {
|
||||
"code": code,
|
||||
"silent": False,
|
||||
"store_history": True,
|
||||
"user_expressions": {},
|
||||
"allow_stdin": False,
|
||||
"stop_on_error": True,
|
||||
},
|
||||
"channel": "shell",
|
||||
}
|
||||
await ws.send(json.dumps(execute_request))
|
||||
await self.execute_in_jupyter(ws)
|
||||
|
||||
stdout, stderr, result = "", "", []
|
||||
|
||||
while True:
|
||||
try:
|
||||
message = await asyncio.wait_for(ws.recv(), timeout)
|
||||
message_data = json.loads(message)
|
||||
if message_data.get("parent_header", {}).get("msg_id") == msg_id:
|
||||
msg_type = message_data.get("msg_type")
|
||||
|
||||
if msg_type == "stream":
|
||||
if message_data["content"]["name"] == "stdout":
|
||||
stdout += message_data["content"]["text"]
|
||||
elif message_data["content"]["name"] == "stderr":
|
||||
stderr += message_data["content"]["text"]
|
||||
|
||||
elif msg_type in ("execute_result", "display_data"):
|
||||
data = message_data["content"]["data"]
|
||||
if "image/png" in data:
|
||||
result.append(
|
||||
f"data:image/png;base64,{data['image/png']}"
|
||||
)
|
||||
elif "text/plain" in data:
|
||||
result.append(data["text/plain"])
|
||||
|
||||
elif msg_type == "error":
|
||||
stderr += "\n".join(message_data["content"]["traceback"])
|
||||
|
||||
elif (
|
||||
msg_type == "status"
|
||||
and message_data["content"]["execution_state"] == "idle"
|
||||
):
|
||||
async def execute_in_jupyter(self, ws) -> None:
|
||||
# send message
|
||||
msg_id = uuid.uuid4().hex
|
||||
await ws.send(
|
||||
json.dumps(
|
||||
{
|
||||
"header": {
|
||||
"msg_id": msg_id,
|
||||
"msg_type": "execute_request",
|
||||
"username": "user",
|
||||
"session": uuid.uuid4().hex,
|
||||
"date": "",
|
||||
"version": "5.3",
|
||||
},
|
||||
"parent_header": {},
|
||||
"metadata": {},
|
||||
"content": {
|
||||
"code": self.code,
|
||||
"silent": False,
|
||||
"store_history": True,
|
||||
"user_expressions": {},
|
||||
"allow_stdin": False,
|
||||
"stop_on_error": True,
|
||||
},
|
||||
"channel": "shell",
|
||||
}
|
||||
)
|
||||
)
|
||||
# parse message
|
||||
stdout, stderr, result = "", "", []
|
||||
while True:
|
||||
try:
|
||||
# wait for message
|
||||
message = await asyncio.wait_for(ws.recv(), self.timeout)
|
||||
message_data = json.loads(message)
|
||||
# msg id not match, skip
|
||||
if message_data.get("parent_header", {}).get("msg_id") != msg_id:
|
||||
continue
|
||||
# check message type
|
||||
msg_type = message_data.get("msg_type")
|
||||
match msg_type:
|
||||
case "stream":
|
||||
if message_data["content"]["name"] == "stdout":
|
||||
stdout += message_data["content"]["text"]
|
||||
elif message_data["content"]["name"] == "stderr":
|
||||
stderr += message_data["content"]["text"]
|
||||
case "execute_result" | "display_data":
|
||||
data = message_data["content"]["data"]
|
||||
if "image/png" in data:
|
||||
result.append(f"data:image/png;base64,{data['image/png']}")
|
||||
elif "text/plain" in data:
|
||||
result.append(data["text/plain"])
|
||||
case "error":
|
||||
stderr += "\n".join(message_data["content"]["traceback"])
|
||||
case "status":
|
||||
if message_data["content"]["execution_state"] == "idle":
|
||||
break
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
stderr += "\nExecution timed out."
|
||||
break
|
||||
except asyncio.TimeoutError:
|
||||
stderr += "\nExecution timed out."
|
||||
break
|
||||
self.result.stdout = stdout.strip()
|
||||
self.result.stderr = stderr.strip()
|
||||
self.result.result = "\n".join(result).strip() if result else ""
|
||||
|
||||
except Exception as e:
|
||||
return {"stdout": "", "stderr": f"Error: {str(e)}", "result": ""}
|
||||
|
||||
finally:
|
||||
if kernel_id:
|
||||
requests.delete(
|
||||
f"{kernel_url}/{kernel_id}", headers=headers, cookies=session.cookies
|
||||
)
|
||||
|
||||
return {
|
||||
"stdout": stdout.strip(),
|
||||
"stderr": stderr.strip(),
|
||||
"result": "\n".join(result).strip() if result else "",
|
||||
}
|
||||
async def execute_code_jupyter(
|
||||
base_url: str, code: str, token: str = "", password: str = "", timeout: int = 60
|
||||
) -> dict:
|
||||
async with JupyterCodeExecuter(
|
||||
base_url, code, token, password, timeout
|
||||
) as executor:
|
||||
result = await executor.run()
|
||||
return result.model_dump()
|
||||
|
||||
@@ -9,7 +9,7 @@ log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
||||
|
||||
|
||||
def get_sorted_filter_ids(model):
|
||||
def get_sorted_filter_ids(model: dict):
|
||||
def get_priority(function_id):
|
||||
function = Functions.get_function_by_id(function_id)
|
||||
if function is not None and hasattr(function, "valves"):
|
||||
@@ -33,12 +33,13 @@ def get_sorted_filter_ids(model):
|
||||
|
||||
|
||||
async def process_filter_functions(
|
||||
request, filter_ids, filter_type, form_data, extra_params
|
||||
request, filter_functions, filter_type, form_data, extra_params
|
||||
):
|
||||
skip_files = None
|
||||
|
||||
for filter_id in filter_ids:
|
||||
filter = Functions.get_function_by_id(filter_id)
|
||||
for function in filter_functions:
|
||||
filter = function
|
||||
filter_id = function.id
|
||||
if not filter:
|
||||
continue
|
||||
|
||||
@@ -48,6 +49,11 @@ async def process_filter_functions(
|
||||
function_module, _, _ = load_function_module_by_id(filter_id)
|
||||
request.app.state.FUNCTIONS[filter_id] = function_module
|
||||
|
||||
# Prepare handler function
|
||||
handler = getattr(function_module, filter_type, None)
|
||||
if not handler:
|
||||
continue
|
||||
|
||||
# Check if the function has a file_handler variable
|
||||
if filter_type == "inlet" and hasattr(function_module, "file_handler"):
|
||||
skip_files = function_module.file_handler
|
||||
@@ -59,11 +65,6 @@ async def process_filter_functions(
|
||||
**(valves if valves else {})
|
||||
)
|
||||
|
||||
# Prepare handler function
|
||||
handler = getattr(function_module, filter_type, None)
|
||||
if not handler:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Prepare parameters
|
||||
sig = inspect.signature(handler)
|
||||
|
||||
@@ -68,6 +68,7 @@ from open_webui.utils.misc import (
|
||||
get_last_user_message,
|
||||
get_last_assistant_message,
|
||||
prepend_to_first_user_message_content,
|
||||
convert_logit_bias_input_to_json,
|
||||
)
|
||||
from open_webui.utils.tools import get_tools
|
||||
from open_webui.utils.plugin import load_function_module_by_id
|
||||
@@ -610,11 +611,18 @@ def apply_params_to_form_data(form_data, model):
|
||||
|
||||
if "reasoning_effort" in params:
|
||||
form_data["reasoning_effort"] = params["reasoning_effort"]
|
||||
if "logit_bias" in params:
|
||||
try:
|
||||
form_data["logit_bias"] = json.loads(
|
||||
convert_logit_bias_input_to_json(params["logit_bias"])
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error parsing logit_bias: {e}")
|
||||
|
||||
return form_data
|
||||
|
||||
|
||||
async def process_chat_payload(request, form_data, metadata, user, model):
|
||||
async def process_chat_payload(request, form_data, user, metadata, model):
|
||||
|
||||
form_data = apply_params_to_form_data(form_data, model)
|
||||
log.debug(f"form_data: {form_data}")
|
||||
@@ -707,9 +715,14 @@ async def process_chat_payload(request, form_data, metadata, user, model):
|
||||
raise e
|
||||
|
||||
try:
|
||||
filter_functions = [
|
||||
Functions.get_function_by_id(filter_id)
|
||||
for filter_id in get_sorted_filter_ids(model)
|
||||
]
|
||||
|
||||
form_data, flags = await process_filter_functions(
|
||||
request=request,
|
||||
filter_ids=get_sorted_filter_ids(model),
|
||||
filter_functions=filter_functions,
|
||||
filter_type="inlet",
|
||||
form_data=form_data,
|
||||
extra_params=extra_params,
|
||||
@@ -856,7 +869,7 @@ async def process_chat_payload(request, form_data, metadata, user, model):
|
||||
|
||||
|
||||
async def process_chat_response(
|
||||
request, response, form_data, user, events, metadata, tasks
|
||||
request, response, form_data, user, metadata, model, events, tasks
|
||||
):
|
||||
async def background_tasks_handler():
|
||||
message_map = Chats.get_messages_by_chat_id(metadata["chat_id"])
|
||||
@@ -1061,9 +1074,14 @@ async def process_chat_response(
|
||||
},
|
||||
"__metadata__": metadata,
|
||||
"__request__": request,
|
||||
"__model__": metadata.get("model"),
|
||||
"__model__": model,
|
||||
}
|
||||
filter_ids = get_sorted_filter_ids(form_data.get("model"))
|
||||
filter_functions = [
|
||||
Functions.get_function_by_id(filter_id)
|
||||
for filter_id in get_sorted_filter_ids(model)
|
||||
]
|
||||
|
||||
print(f"{filter_functions=}")
|
||||
|
||||
# Streaming response
|
||||
if event_emitter and event_caller:
|
||||
@@ -1470,7 +1488,7 @@ async def process_chat_response(
|
||||
|
||||
data, _ = await process_filter_functions(
|
||||
request=request,
|
||||
filter_ids=filter_ids,
|
||||
filter_functions=filter_functions,
|
||||
filter_type="stream",
|
||||
form_data=data,
|
||||
extra_params=extra_params,
|
||||
@@ -1544,9 +1562,59 @@ async def process_chat_response(
|
||||
|
||||
value = delta.get("content")
|
||||
|
||||
if value:
|
||||
content = f"{content}{value}"
|
||||
reasoning_content = delta.get("reasoning_content")
|
||||
if reasoning_content:
|
||||
if (
|
||||
not content_blocks
|
||||
or content_blocks[-1]["type"] != "reasoning"
|
||||
):
|
||||
reasoning_block = {
|
||||
"type": "reasoning",
|
||||
"start_tag": "think",
|
||||
"end_tag": "/think",
|
||||
"attributes": {
|
||||
"type": "reasoning_content"
|
||||
},
|
||||
"content": "",
|
||||
"started_at": time.time(),
|
||||
}
|
||||
content_blocks.append(reasoning_block)
|
||||
else:
|
||||
reasoning_block = content_blocks[-1]
|
||||
|
||||
reasoning_block["content"] += reasoning_content
|
||||
|
||||
data = {
|
||||
"content": serialize_content_blocks(
|
||||
content_blocks
|
||||
)
|
||||
}
|
||||
|
||||
if value:
|
||||
if (
|
||||
content_blocks
|
||||
and content_blocks[-1]["type"]
|
||||
== "reasoning"
|
||||
and content_blocks[-1]
|
||||
.get("attributes", {})
|
||||
.get("type")
|
||||
== "reasoning_content"
|
||||
):
|
||||
reasoning_block = content_blocks[-1]
|
||||
reasoning_block["ended_at"] = time.time()
|
||||
reasoning_block["duration"] = int(
|
||||
reasoning_block["ended_at"]
|
||||
- reasoning_block["started_at"]
|
||||
)
|
||||
|
||||
content_blocks.append(
|
||||
{
|
||||
"type": "text",
|
||||
"content": "",
|
||||
}
|
||||
)
|
||||
|
||||
content = f"{content}{value}"
|
||||
if not content_blocks:
|
||||
content_blocks.append(
|
||||
{
|
||||
@@ -2017,7 +2085,7 @@ async def process_chat_response(
|
||||
for event in events:
|
||||
event, _ = await process_filter_functions(
|
||||
request=request,
|
||||
filter_ids=filter_ids,
|
||||
filter_functions=filter_functions,
|
||||
filter_type="stream",
|
||||
form_data=event,
|
||||
extra_params=extra_params,
|
||||
@@ -2029,7 +2097,7 @@ async def process_chat_response(
|
||||
async for data in original_generator:
|
||||
data, _ = await process_filter_functions(
|
||||
request=request,
|
||||
filter_ids=filter_ids,
|
||||
filter_functions=filter_functions,
|
||||
filter_type="stream",
|
||||
form_data=data,
|
||||
extra_params=extra_params,
|
||||
|
||||
@@ -6,6 +6,7 @@ import logging
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional
|
||||
import json
|
||||
|
||||
|
||||
import collections.abc
|
||||
@@ -450,3 +451,15 @@ def parse_ollama_modelfile(model_text):
|
||||
data["params"]["messages"] = messages
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def convert_logit_bias_input_to_json(user_input):
|
||||
logit_bias_pairs = user_input.split(",")
|
||||
logit_bias_json = {}
|
||||
for pair in logit_bias_pairs:
|
||||
token, bias = pair.split(":")
|
||||
token = str(token.strip())
|
||||
bias = int(bias.strip())
|
||||
bias = 100 if bias > 100 else -100 if bias < -100 else bias
|
||||
logit_bias_json[token] = bias
|
||||
return json.dumps(logit_bias_json)
|
||||
|
||||
@@ -234,7 +234,7 @@ class OAuthManager:
|
||||
log.warning(f"OAuth callback error: {e}")
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||
user_data: UserInfo = token.get("userinfo")
|
||||
if not user_data or "email" not in user_data:
|
||||
if not user_data or auth_manager_config.OAUTH_EMAIL_CLAIM not in user_data:
|
||||
user_data: UserInfo = await client.userinfo(token=token)
|
||||
if not user_data:
|
||||
log.warning(f"OAuth callback failed, user data is missing: {token}")
|
||||
|
||||
@@ -62,6 +62,7 @@ def apply_model_params_to_body_openai(params: dict, form_data: dict) -> dict:
|
||||
"reasoning_effort": str,
|
||||
"seed": lambda x: x,
|
||||
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
|
||||
"logit_bias": lambda x: x,
|
||||
}
|
||||
return apply_model_params_to_body(params, form_data, mappings)
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ class PDFGenerator:
|
||||
# When running using `pip install -e .` the static directory is in the site packages.
|
||||
# This path only works if `open-webui serve` is run from the root of this project.
|
||||
if not FONTS_DIR.exists():
|
||||
FONTS_DIR = Path("./backend/static/fonts")
|
||||
FONTS_DIR = Path(".") / "backend" / "static" / "fonts"
|
||||
|
||||
pdf.add_font("NotoSans", "", f"{FONTS_DIR}/NotoSans-Regular.ttf")
|
||||
pdf.add_font("NotoSans", "b", f"{FONTS_DIR}/NotoSans-Bold.ttf")
|
||||
|
||||
@@ -104,7 +104,7 @@ def replace_prompt_variable(template: str, prompt: str) -> str:
|
||||
|
||||
|
||||
def replace_messages_variable(
|
||||
template: str, messages: Optional[list[str]] = None
|
||||
template: str, messages: Optional[list[dict]] = None
|
||||
) -> str:
|
||||
def replacement_function(match):
|
||||
full_match = match.group(0)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
fastapi==0.115.7
|
||||
uvicorn[standard]==0.30.6
|
||||
uvicorn[standard]==0.34.0
|
||||
pydantic==2.10.6
|
||||
python-multipart==0.0.18
|
||||
|
||||
@@ -13,14 +13,14 @@ async-timeout
|
||||
aiocache
|
||||
aiofiles
|
||||
|
||||
sqlalchemy==2.0.32
|
||||
sqlalchemy==2.0.38
|
||||
alembic==1.14.0
|
||||
peewee==3.17.8
|
||||
peewee==3.17.9
|
||||
peewee-migrate==1.12.2
|
||||
psycopg2-binary==2.9.9
|
||||
pgvector==0.3.5
|
||||
PyMySQL==1.1.1
|
||||
bcrypt==4.2.0
|
||||
bcrypt==4.3.0
|
||||
|
||||
pymongo
|
||||
redis
|
||||
@@ -40,8 +40,8 @@ anthropic
|
||||
google-generativeai==0.7.2
|
||||
tiktoken
|
||||
|
||||
langchain==0.3.7
|
||||
langchain-community==0.3.7
|
||||
langchain==0.3.19
|
||||
langchain-community==0.3.18
|
||||
|
||||
fake-useragent==1.5.1
|
||||
chromadb==0.6.2
|
||||
@@ -49,6 +49,8 @@ pymilvus==2.5.0
|
||||
qdrant-client~=1.12.0
|
||||
opensearch-py==2.8.0
|
||||
playwright==1.49.1 # Caution: version must match docker-compose.playwright.yaml
|
||||
elasticsearch==8.17.1
|
||||
|
||||
|
||||
transformers
|
||||
sentence-transformers==3.3.1
|
||||
@@ -85,7 +87,7 @@ faster-whisper==1.1.1
|
||||
PyJWT[crypto]==2.10.1
|
||||
authlib==1.4.1
|
||||
|
||||
black==24.8.0
|
||||
black==25.1.0
|
||||
langfuse==2.44.0
|
||||
youtube-transcript-api==0.6.3
|
||||
pytube==15.0.0
|
||||
|
||||