Unverified Commit 3b3d0cce authored by Jannik S's avatar Jannik S Committed by GitHub
Browse files

Merge branch 'dev' into dockerfile-optimisation

parents 073f06d4 72110c29
...@@ -10,3 +10,7 @@ OPENAI_API_KEY='' ...@@ -10,3 +10,7 @@ OPENAI_API_KEY=''
# DO NOT TRACK # DO NOT TRACK
SCARF_NO_ANALYTICS=true SCARF_NO_ANALYTICS=true
DO_NOT_TRACK=true DO_NOT_TRACK=true
# Use locally bundled version of the LiteLLM cost map json
# to avoid repetitive startup connections
LITELLM_LOCAL_MODEL_COST_MAP="True"
...@@ -57,3 +57,14 @@ jobs: ...@@ -57,3 +57,14 @@ jobs:
path: . path: .
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger Docker build workflow
uses: actions/github-script@v7
with:
script: |
github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'docker-build.yaml',
ref: 'v${{ steps.get_version.outputs.version }}',
})
...@@ -2,6 +2,7 @@ name: Create and publish Docker images with specific build args ...@@ -2,6 +2,7 @@ name: Create and publish Docker images with specific build args
# Configures this workflow to run every time a change is pushed to the branch called `release`. # Configures this workflow to run every time a change is pushed to the branch called `release`.
on: on:
workflow_dispatch:
push: push:
branches: branches:
- main - main
......
...@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file. ...@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.117] - 2024-04-03
### Added
- 🗨️ **Local Chat Sharing**: Share chat links seamlessly between users.
- 🔑 **API Key Generation Support**: Generate secret keys to leverage Open WebUI with OpenAI libraries.
- 📄 **Chat Download as PDF**: Easily download chats in PDF format.
- 📝 **Improved Logging**: Enhancements to logging functionality.
- 📧 **Trusted Email Authentication**: Authenticate using a trusted email header.
### Fixed
- 🌷 **Enhanced Dutch Translation**: Improved translation for Dutch users.
-**White Theme Styling**: Resolved styling issue with the white theme.
- 📜 **LaTeX Chat Screen Overflow**: Fixed screen overflow issue with LaTeX rendering.
- 🔒 **Security Patches**: Applied necessary security patches.
## [0.1.116] - 2024-03-31 ## [0.1.116] - 2024-03-31
### Added ### Added
......
...@@ -215,7 +215,8 @@ async def get_ollama_versions(url_idx: Optional[int] = None): ...@@ -215,7 +215,8 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
if len(responses) > 0: if len(responses) > 0:
lowest_version = min( lowest_version = min(
responses, key=lambda x: tuple(map(int, x["version"].split("."))) responses,
key=lambda x: tuple(map(int, x["version"].split("-")[0].split("."))),
) )
return {"version": lowest_version["version"]} return {"version": lowest_version["version"]}
......
...@@ -8,7 +8,7 @@ from fastapi import ( ...@@ -8,7 +8,7 @@ from fastapi import (
Form, Form,
) )
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
import os, shutil, logging import os, shutil, logging, re
from pathlib import Path from pathlib import Path
from typing import List from typing import List
...@@ -438,25 +438,11 @@ def store_doc( ...@@ -438,25 +438,11 @@ def store_doc(
log.info(f"file.content_type: {file.content_type}") log.info(f"file.content_type: {file.content_type}")
try: try:
is_valid_filename = True
unsanitized_filename = file.filename unsanitized_filename = file.filename
if not unsanitized_filename.isascii(): filename = os.path.basename(unsanitized_filename)
is_valid_filename = False
unvalidated_file_path = f"{UPLOAD_DIR}/{unsanitized_filename}" file_path = f"{UPLOAD_DIR}/{filename}"
dereferenced_file_path = str(Path(unvalidated_file_path).resolve(strict=False))
if not dereferenced_file_path.startswith(UPLOAD_DIR):
is_valid_filename = False
if is_valid_filename:
file_path = dereferenced_file_path
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(),
)
filename = file.filename
contents = file.file.read() contents = file.file.read()
with open(file_path, "wb") as f: with open(file_path, "wb") as f:
f.write(contents) f.write(contents)
...@@ -467,7 +453,7 @@ def store_doc( ...@@ -467,7 +453,7 @@ def store_doc(
collection_name = calculate_sha256(f)[:63] collection_name = calculate_sha256(f)[:63]
f.close() f.close()
loader, known_type = get_loader(file.filename, file.content_type, file_path) loader, known_type = get_loader(filename, file.content_type, file_path)
data = loader.load() data = loader.load()
try: try:
......
...@@ -86,6 +86,7 @@ class SignupForm(BaseModel): ...@@ -86,6 +86,7 @@ class SignupForm(BaseModel):
name: str name: str
email: str email: str
password: str password: str
profile_image_url: Optional[str] = "/user.png"
class AuthsTable: class AuthsTable:
...@@ -94,7 +95,12 @@ class AuthsTable: ...@@ -94,7 +95,12 @@ class AuthsTable:
self.db.create_tables([Auth]) self.db.create_tables([Auth])
def insert_new_auth( def insert_new_auth(
self, email: str, password: str, name: str, role: str = "pending" self,
email: str,
password: str,
name: str,
profile_image_url: str = "/user.png",
role: str = "pending",
) -> Optional[UserModel]: ) -> Optional[UserModel]:
log.info("insert_new_auth") log.info("insert_new_auth")
...@@ -105,7 +111,7 @@ class AuthsTable: ...@@ -105,7 +111,7 @@ class AuthsTable:
) )
result = Auth.create(**auth.model_dump()) result = Auth.create(**auth.model_dump())
user = Users.insert_new_user(id, name, email, role) user = Users.insert_new_user(id, name, email, profile_image_url, role)
if result and user: if result and user:
return user return user
......
...@@ -206,6 +206,18 @@ class ChatTable: ...@@ -206,6 +206,18 @@ class ChatTable:
except: except:
return None return None
def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]:
try:
chat = Chat.get(Chat.share_id == id)
if chat:
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
else:
return None
except:
return None
def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]: def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]:
try: try:
chat = Chat.get(Chat.id == id, Chat.user_id == user_id) chat = Chat.get(Chat.id == id, Chat.user_id == user_id)
......
...@@ -31,7 +31,7 @@ class UserModel(BaseModel): ...@@ -31,7 +31,7 @@ class UserModel(BaseModel):
name: str name: str
email: str email: str
role: str = "pending" role: str = "pending"
profile_image_url: str = "/user.png" profile_image_url: str
timestamp: int # timestamp in epoch timestamp: int # timestamp in epoch
api_key: Optional[str] = None api_key: Optional[str] = None
...@@ -59,7 +59,12 @@ class UsersTable: ...@@ -59,7 +59,12 @@ class UsersTable:
self.db.create_tables([User]) self.db.create_tables([User])
def insert_new_user( def insert_new_user(
self, id: str, name: str, email: str, role: str = "pending" self,
id: str,
name: str,
email: str,
profile_image_url: str = "/user.png",
role: str = "pending",
) -> Optional[UserModel]: ) -> Optional[UserModel]:
user = UserModel( user = UserModel(
**{ **{
...@@ -67,7 +72,7 @@ class UsersTable: ...@@ -67,7 +72,7 @@ class UsersTable:
"name": name, "name": name,
"email": email, "email": email,
"role": role, "role": role,
"profile_image_url": "/user.png", "profile_image_url": profile_image_url,
"timestamp": int(time.time()), "timestamp": int(time.time()),
} }
) )
......
...@@ -163,7 +163,11 @@ async def signup(request: Request, form_data: SignupForm): ...@@ -163,7 +163,11 @@ async def signup(request: Request, form_data: SignupForm):
) )
hashed = get_password_hash(form_data.password) hashed = get_password_hash(form_data.password)
user = Auths.insert_new_auth( user = Auths.insert_new_auth(
form_data.email.lower(), hashed, form_data.name, role form_data.email.lower(),
hashed,
form_data.name,
form_data.profile_image_url,
role,
) )
if user: if user:
......
...@@ -251,6 +251,14 @@ async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)): ...@@ -251,6 +251,14 @@ async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)):
@router.get("/share/{share_id}", response_model=Optional[ChatResponse]) @router.get("/share/{share_id}", response_model=Optional[ChatResponse])
async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)): async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)):
if user.role == "pending":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role == "user":
chat = Chats.get_chat_by_share_id(share_id)
elif user.role == "admin":
chat = Chats.get_chat_by_id(share_id) chat = Chats.get_chat_by_id(share_id)
if chat: if chat:
......
from fastapi import APIRouter, UploadFile, File, BackgroundTasks from fastapi import APIRouter, UploadFile, File, Response
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from starlette.responses import StreamingResponse, FileResponse from starlette.responses import StreamingResponse, FileResponse
from pydantic import BaseModel from pydantic import BaseModel
import requests
import os from fpdf import FPDF
import aiohttp import markdown
import json
from utils.utils import get_admin_user from utils.utils import get_admin_user
...@@ -16,7 +13,7 @@ from utils.misc import calculate_sha256, get_gravatar_url ...@@ -16,7 +13,7 @@ from utils.misc import calculate_sha256, get_gravatar_url
from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR
from constants import ERROR_MESSAGES from constants import ERROR_MESSAGES
from typing import List
router = APIRouter() router = APIRouter()
...@@ -28,6 +25,70 @@ async def get_gravatar( ...@@ -28,6 +25,70 @@ async def get_gravatar(
return get_gravatar_url(email) return get_gravatar_url(email)
class MarkdownForm(BaseModel):
md: str
@router.post("/markdown")
async def get_html_from_markdown(
form_data: MarkdownForm,
):
return {"html": markdown.markdown(form_data.md)}
class ChatForm(BaseModel):
title: str
messages: List[dict]
@router.post("/pdf")
async def download_chat_as_pdf(
form_data: ChatForm,
):
pdf = FPDF()
pdf.add_page()
STATIC_DIR = "./static"
FONTS_DIR = f"{STATIC_DIR}/fonts"
pdf.add_font("NotoSans", "", f"{FONTS_DIR}/NotoSans-Regular.ttf")
pdf.add_font("NotoSans", "b", f"{FONTS_DIR}/NotoSans-Bold.ttf")
pdf.add_font("NotoSans", "i", f"{FONTS_DIR}/NotoSans-Italic.ttf")
pdf.add_font("NotoSansKR", "", f"{FONTS_DIR}/NotoSansKR-Regular.ttf")
pdf.add_font("NotoSansJP", "", f"{FONTS_DIR}/NotoSansJP-Regular.ttf")
pdf.set_font("NotoSans", size=12)
pdf.set_fallback_fonts(["NotoSansKR", "NotoSansJP"])
pdf.set_auto_page_break(auto=True, margin=15)
# Adjust the effective page width for multi_cell
effective_page_width = (
pdf.w - 2 * pdf.l_margin - 10
) # Subtracted an additional 10 for extra padding
# Add chat messages
for message in form_data.messages:
role = message["role"]
content = message["content"]
pdf.set_font("NotoSans", "B", size=14) # Bold for the role
pdf.multi_cell(effective_page_width, 10, f"{role.upper()}", 0, "L")
pdf.ln(1) # Extra space between messages
pdf.set_font("NotoSans", size=10) # Regular for content
pdf.multi_cell(effective_page_width, 6, content, 0, "L")
pdf.ln(1.5) # Extra space between messages
# Save the pdf with name .pdf
pdf_bytes = pdf.output()
return Response(
content=bytes(pdf_bytes),
media_type="application/pdf",
headers={"Content-Disposition": f"attachment;filename=chat.pdf"},
)
@router.get("/db/download") @router.get("/db/download")
async def download_db(user=Depends(get_admin_user)): async def download_db(user=Depends(get_admin_user)):
......
...@@ -25,8 +25,9 @@ try: ...@@ -25,8 +25,9 @@ try:
except ImportError: except ImportError:
log.warning("dotenv not installed, skipping...") log.warning("dotenv not installed, skipping...")
WEBUI_NAME = "Open WebUI" WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png" WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
shutil.copyfile("../build/favicon.png", "./static/favicon.png") shutil.copyfile("../build/favicon.png", "./static/favicon.png")
#################################### ####################################
...@@ -149,6 +150,7 @@ log.setLevel(SRC_LOG_LEVELS["CONFIG"]) ...@@ -149,6 +150,7 @@ log.setLevel(SRC_LOG_LEVELS["CONFIG"])
#################################### ####################################
CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "") CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "")
if CUSTOM_NAME: if CUSTOM_NAME:
try: try:
r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}") r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}")
...@@ -171,7 +173,9 @@ if CUSTOM_NAME: ...@@ -171,7 +173,9 @@ if CUSTOM_NAME:
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
pass pass
else:
if WEBUI_NAME != "Open WebUI":
WEBUI_NAME += " (Open WebUI)"
#################################### ####################################
# DATA/FRONTEND BUILD DIR # DATA/FRONTEND BUILD DIR
......
...@@ -84,7 +84,6 @@ app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST ...@@ -84,7 +84,6 @@ app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.WEBHOOK_URL = WEBHOOK_URL app.state.WEBHOOK_URL = WEBHOOK_URL
origins = ["*"] origins = ["*"]
...@@ -284,6 +283,20 @@ async def get_app_latest_release_version(): ...@@ -284,6 +283,20 @@ async def get_app_latest_release_version():
) )
@app.get("/manifest.json")
async def get_manifest_json():
return {
"name": WEBUI_NAME,
"short_name": WEBUI_NAME,
"start_url": "/",
"display": "standalone",
"background_color": "#343541",
"theme_color": "#343541",
"orientation": "portrait-primary",
"icons": [{"src": "/favicon.png", "type": "image/png", "sizes": "844x884"}],
}
app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/static", StaticFiles(directory="static"), name="static")
app.mount("/cache", StaticFiles(directory="data/cache"), name="cache") app.mount("/cache", StaticFiles(directory="data/cache"), name="cache")
......
...@@ -18,6 +18,8 @@ peewee-migrate ...@@ -18,6 +18,8 @@ peewee-migrate
bcrypt bcrypt
litellm==1.30.7 litellm==1.30.7
boto3
argon2-cffi argon2-cffi
apscheduler apscheduler
google-generativeai google-generativeai
...@@ -40,6 +42,8 @@ xlrd ...@@ -40,6 +42,8 @@ xlrd
opencv-python-headless opencv-python-headless
rapidocr-onnxruntime rapidocr-onnxruntime
fpdf2
faster-whisper faster-whisper
PyJWT PyJWT
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment