diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e32e1f89..86ff57384 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ 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/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.12] - 2025-02-13 + +### Added + +- **🛠️ Multiple Tool Calls Support for Native Function Mode**: Functions now can call multiple tools within a single response, unlocking better automation and workflow flexibility when using native function calling. + +### Fixed + +- **📝 Playground Text Completion Restored**: Addressed an issue where text completion in the Playground was not functioning. +- **🔗 Direct Connections Now Work for Regular Users**: Fixed a bug where users with the 'user' role couldn't establish direct API connections, enabling seamless model usage for all user tiers. +- **⚡ Landing Page Input No Longer Lags with Long Text**: Improved input responsiveness on the landing page, ensuring fast and smooth typing experiences even when entering long messages. +- **🔧 Parameter in Functions Fixed**: Fixed an issue where the reserved parameters wasn’t recognized within functions, restoring full functionality for advanced task-based automation. + ## [0.5.11] - 2025-02-13 ### Added diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index ff298dc5b..adfdcfec8 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1190,6 +1190,12 @@ ENABLE_TAGS_GENERATION = PersistentConfig( os.environ.get("ENABLE_TAGS_GENERATION", "True").lower() == "true", ) +ENABLE_TITLE_GENERATION = PersistentConfig( + "ENABLE_TITLE_GENERATION", + "task.title.enable", + os.environ.get("ENABLE_TITLE_GENERATION", "True").lower() == "true", +) + ENABLE_SEARCH_QUERY_GENERATION = PersistentConfig( "ENABLE_SEARCH_QUERY_GENERATION", @@ -1803,6 +1809,18 @@ SEARCHAPI_ENGINE = PersistentConfig( os.getenv("SEARCHAPI_ENGINE", ""), ) +SERPAPI_API_KEY = PersistentConfig( + "SERPAPI_API_KEY", + "rag.web.search.serpapi_api_key", + os.getenv("SERPAPI_API_KEY", ""), +) + +SERPAPI_ENGINE = PersistentConfig( + "SERPAPI_ENGINE", + "rag.web.search.serpapi_engine", + os.getenv("SERPAPI_ENGINE", ""), +) + BING_SEARCH_V7_ENDPOINT = PersistentConfig( "BING_SEARCH_V7_ENDPOINT", "rag.web.search.bing_search_v7_endpoint", diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 88b5b3f69..a36323151 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -179,6 +179,8 @@ from open_webui.config import ( JINA_API_KEY, SEARCHAPI_API_KEY, SEARCHAPI_ENGINE, + SERPAPI_API_KEY, + SERPAPI_ENGINE, SEARXNG_QUERY_URL, SERPER_API_KEY, SERPLY_API_KEY, @@ -264,6 +266,7 @@ from open_webui.config import ( TASK_MODEL, TASK_MODEL_EXTERNAL, ENABLE_TAGS_GENERATION, + ENABLE_TITLE_GENERATION, ENABLE_SEARCH_QUERY_GENERATION, ENABLE_RETRIEVAL_QUERY_GENERATION, ENABLE_AUTOCOMPLETE_GENERATION, @@ -546,6 +549,8 @@ app.state.config.SERPLY_API_KEY = SERPLY_API_KEY app.state.config.TAVILY_API_KEY = TAVILY_API_KEY app.state.config.SEARCHAPI_API_KEY = SEARCHAPI_API_KEY app.state.config.SEARCHAPI_ENGINE = SEARCHAPI_ENGINE +app.state.config.SERPAPI_API_KEY = SERPAPI_API_KEY +app.state.config.SERPAPI_ENGINE = SERPAPI_ENGINE 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 @@ -689,6 +694,7 @@ app.state.config.ENABLE_SEARCH_QUERY_GENERATION = ENABLE_SEARCH_QUERY_GENERATION app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION = ENABLE_RETRIEVAL_QUERY_GENERATION app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = ENABLE_AUTOCOMPLETE_GENERATION app.state.config.ENABLE_TAGS_GENERATION = ENABLE_TAGS_GENERATION +app.state.config.ENABLE_TITLE_GENERATION = ENABLE_TITLE_GENERATION app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE diff --git a/backend/open_webui/retrieval/web/serpapi.py b/backend/open_webui/retrieval/web/serpapi.py new file mode 100644 index 000000000..028b6bcfe --- /dev/null +++ b/backend/open_webui/retrieval/web/serpapi.py @@ -0,0 +1,48 @@ +import logging +from typing import Optional +from urllib.parse import urlencode + +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_serpapi( + api_key: str, + engine: str, + query: str, + count: int, + filter_list: Optional[list[str]] = None, +) -> list[SearchResult]: + """Search using serpapi.com's API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A serpapi.com API key + query (str): The query to search for + """ + url = "https://serpapi.com/search" + + engine = engine or "google" + + payload = {"engine": engine, "q": query, "api_key": api_key} + + url = f"{url}?{urlencode(payload)}" + response = requests.request("GET", url) + + json_response = response.json() + log.info(f"results from serpapi search: {json_response}") + + results = sorted( + json_response.get("organic_results", []), key=lambda x: x.get("position", 0) + ) + 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] + ] diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index e4bab5289..415d3bbb5 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -50,6 +50,7 @@ from open_webui.retrieval.web.duckduckgo import search_duckduckgo from open_webui.retrieval.web.google_pse import search_google_pse from open_webui.retrieval.web.jina_search import search_jina from open_webui.retrieval.web.searchapi import search_searchapi +from open_webui.retrieval.web.serpapi import search_serpapi from open_webui.retrieval.web.searxng import search_searxng from open_webui.retrieval.web.serper import search_serper from open_webui.retrieval.web.serply import search_serply @@ -388,6 +389,8 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): "tavily_api_key": request.app.state.config.TAVILY_API_KEY, "searchapi_api_key": request.app.state.config.SEARCHAPI_API_KEY, "searchapi_engine": request.app.state.config.SEARCHAPI_ENGINE, + "serpapi_api_key": request.app.state.config.SERPAPI_API_KEY, + "serpapi_engine": request.app.state.config.SERPAPI_ENGINE, "jina_api_key": request.app.state.config.JINA_API_KEY, "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, @@ -439,6 +442,8 @@ class WebSearchConfig(BaseModel): tavily_api_key: Optional[str] = None searchapi_api_key: Optional[str] = None searchapi_engine: Optional[str] = None + serpapi_api_key: Optional[str] = None + serpapi_engine: Optional[str] = None jina_api_key: Optional[str] = None bing_search_v7_endpoint: Optional[str] = None bing_search_v7_subscription_key: Optional[str] = None @@ -545,6 +550,9 @@ async def update_rag_config( form_data.web.search.searchapi_engine ) + request.app.state.config.SERPAPI_API_KEY = form_data.web.search.serpapi_api_key + request.app.state.config.SERPAPI_ENGINE = form_data.web.search.serpapi_engine + request.app.state.config.JINA_API_KEY = form_data.web.search.jina_api_key request.app.state.config.BING_SEARCH_V7_ENDPOINT = ( form_data.web.search.bing_search_v7_endpoint @@ -604,6 +612,8 @@ async def update_rag_config( "serply_api_key": request.app.state.config.SERPLY_API_KEY, "serachapi_api_key": request.app.state.config.SEARCHAPI_API_KEY, "searchapi_engine": request.app.state.config.SEARCHAPI_ENGINE, + "serpapi_api_key": request.app.state.config.SERPAPI_API_KEY, + "serpapi_engine": request.app.state.config.SERPAPI_ENGINE, "tavily_api_key": request.app.state.config.TAVILY_API_KEY, "jina_api_key": request.app.state.config.JINA_API_KEY, "bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT, @@ -760,7 +770,11 @@ def save_docs_to_vector_db( # for meta-data so convert them to string. for metadata in metadatas: for key, value in metadata.items(): - if isinstance(value, datetime): + if ( + isinstance(value, datetime) + or isinstance(value, list) + or isinstance(value, dict) + ): metadata[key] = str(value) try: @@ -1127,6 +1141,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]: - TAVILY_API_KEY - EXA_API_KEY - SEARCHAPI_API_KEY + SEARCHAPI_ENGINE (by default `google`) + - SERPAPI_API_KEY + SERPAPI_ENGINE (by default `google`) Args: query (str): The query to search for """ @@ -1255,6 +1270,17 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]: ) else: raise Exception("No SEARCHAPI_API_KEY found in environment variables") + elif engine == "serpapi": + if request.app.state.config.SERPAPI_API_KEY: + return search_serpapi( + request.app.state.config.SERPAPI_API_KEY, + request.app.state.config.SERPAPI_ENGINE, + query, + request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, + request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception("No SERPAPI_API_KEY found in environment variables") elif engine == "jina": return search_jina( request.app.state.config.JINA_API_KEY, diff --git a/backend/open_webui/routers/tasks.py b/backend/open_webui/routers/tasks.py index 91ec8e972..8b17c6c4b 100644 --- a/backend/open_webui/routers/tasks.py +++ b/backend/open_webui/routers/tasks.py @@ -58,6 +58,7 @@ async def get_task_config(request: Request, user=Depends(get_verified_user)): "AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH": request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH, "TAGS_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE, "ENABLE_TAGS_GENERATION": request.app.state.config.ENABLE_TAGS_GENERATION, + "ENABLE_TITLE_GENERATION": request.app.state.config.ENABLE_TITLE_GENERATION, "ENABLE_SEARCH_QUERY_GENERATION": request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION, "ENABLE_RETRIEVAL_QUERY_GENERATION": request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION, "QUERY_GENERATION_PROMPT_TEMPLATE": request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE, @@ -68,6 +69,7 @@ async def get_task_config(request: Request, user=Depends(get_verified_user)): class TaskConfigForm(BaseModel): TASK_MODEL: Optional[str] TASK_MODEL_EXTERNAL: Optional[str] + ENABLE_TITLE_GENERATION: bool TITLE_GENERATION_PROMPT_TEMPLATE: str IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE: str ENABLE_AUTOCOMPLETE_GENERATION: bool @@ -86,6 +88,7 @@ async def update_task_config( ): request.app.state.config.TASK_MODEL = form_data.TASK_MODEL request.app.state.config.TASK_MODEL_EXTERNAL = form_data.TASK_MODEL_EXTERNAL + request.app.state.config.ENABLE_TITLE_GENERATION = form_data.ENABLE_TITLE_GENERATION request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = ( form_data.TITLE_GENERATION_PROMPT_TEMPLATE ) @@ -122,6 +125,7 @@ async def update_task_config( return { "TASK_MODEL": request.app.state.config.TASK_MODEL, "TASK_MODEL_EXTERNAL": request.app.state.config.TASK_MODEL_EXTERNAL, + "ENABLE_TITLE_GENERATION": request.app.state.config.ENABLE_TITLE_GENERATION, "TITLE_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE, "IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE": request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE, "ENABLE_AUTOCOMPLETE_GENERATION": request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION, @@ -139,6 +143,13 @@ async def update_task_config( async def generate_title( request: Request, form_data: dict, user=Depends(get_verified_user) ): + + if not request.app.state.config.ENABLE_TITLE_GENERATION: + return JSONResponse( + status_code=status.HTTP_200_OK, + content={"detail": "Title generation is disabled"}, + ) + if getattr(request.state, "direct", False) and hasattr(request.state, "model"): models = { request.state.model["id"]: request.state.model, diff --git a/backend/open_webui/utils/chat.py b/backend/open_webui/utils/chat.py index 253eaedfb..569bcad85 100644 --- a/backend/open_webui/utils/chat.py +++ b/backend/open_webui/utils/chat.py @@ -161,11 +161,18 @@ async def generate_chat_completion( user: Any, bypass_filter: bool = False, ): + log.debug(f"generate_chat_completion: {form_data}") if BYPASS_MODEL_ACCESS_CONTROL: bypass_filter = True if hasattr(request.state, "metadata"): - form_data["metadata"] = request.state.metadata + if "metadata" not in form_data: + form_data["metadata"] = request.state.metadata + else: + form_data["metadata"] = { + **form_data["metadata"], + **request.state.metadata, + } if getattr(request.state, "direct", False) and hasattr(request.state, "model"): models = { @@ -187,19 +194,18 @@ async def generate_chat_completion( model = models[model_id] - # Check if user has access to the model - if not bypass_filter and user.role == "user": - try: - check_model_access(user, model) - except Exception as e: - raise e - if getattr(request.state, "direct", False): return await generate_direct_chat_completion( request, form_data, user=user, models=models ) - else: + # Check if user has access to the model + if not bypass_filter and user.role == "user": + try: + check_model_access(user, model) + except Exception as e: + raise e + if model["owned_by"] == "arena": model_ids = model.get("info", {}).get("meta", {}).get("model_ids") filter_mode = model.get("info", {}).get("meta", {}).get("filter_mode") diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 4d70ddd65..4e4ba8d30 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -1151,6 +1151,46 @@ async def process_chat_response( return content.strip() + def convert_content_blocks_to_messages(content_blocks): + messages = [] + + temp_blocks = [] + for idx, block in enumerate(content_blocks): + if block["type"] == "tool_calls": + messages.append( + { + "role": "assistant", + "content": serialize_content_blocks(temp_blocks), + "tool_calls": block.get("content"), + } + ) + + results = block.get("results", []) + + for result in results: + messages.append( + { + "role": "tool", + "tool_call_id": result["tool_call_id"], + "content": result["content"], + } + ) + temp_blocks = [] + else: + temp_blocks.append(block) + + if temp_blocks: + content = serialize_content_blocks(temp_blocks) + if content: + messages.append( + { + "role": "assistant", + "content": content, + } + ) + + return messages + def tag_content_handler(content_type, tags, content, content_blocks): end_flag = False @@ -1542,7 +1582,6 @@ async def process_chat_response( results = [] for tool_call in response_tool_calls: - print("\n\n" + str(tool_call) + "\n\n") tool_call_id = tool_call.get("id", "") tool_name = tool_call.get("function", {}).get("name", "") @@ -1608,23 +1647,10 @@ async def process_chat_response( { "model": model_id, "stream": True, + "tools": form_data["tools"], "messages": [ *form_data["messages"], - { - "role": "assistant", - "content": serialize_content_blocks( - content_blocks, raw=True - ), - "tool_calls": response_tool_calls, - }, - *[ - { - "role": "tool", - "tool_call_id": result["tool_call_id"], - "content": result["content"], - } - for result in results - ], + *convert_content_blocks_to_messages(content_blocks), ], }, user, diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index 4eace24dc..f79b62684 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -225,10 +225,11 @@ def openai_chat_completion_message_template( template = openai_chat_message_template(model) template["object"] = "chat.completion" if message is not None: - template["choices"][0]["message"] = {"content": message, "role": "assistant"} - - if tool_calls: - template["choices"][0]["tool_calls"] = tool_calls + template["choices"][0]["message"] = { + "content": message, + "role": "assistant", + **({"tool_calls": tool_calls} if tool_calls else {}), + } template["choices"][0]["finish_reason"] = "stop" diff --git a/backend/requirements.txt b/backend/requirements.txt index 3567924a8..9b859b84a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -3,9 +3,6 @@ uvicorn[standard]==0.30.6 pydantic==2.9.2 python-multipart==0.0.18 -Flask==3.1.0 -Flask-Cors==5.0.0 - python-socketio==5.11.3 python-jose==3.3.0 passlib[bcrypt]==1.7.4 diff --git a/package-lock.json b/package-lock.json index 710ee7fce..56a76c09c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.5.11", + "version": "0.5.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.5.11", + "version": "0.5.12", "dependencies": { "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-python": "^6.1.6", diff --git a/package.json b/package.json index 9a481f057..c1a76fd78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.5.11", + "version": "0.5.12", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", diff --git a/pyproject.toml b/pyproject.toml index f4261ba82..dac8bbf78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,9 +11,6 @@ dependencies = [ "pydantic==2.9.2", "python-multipart==0.0.18", - "Flask==3.1.0", - "Flask-Cors==5.0.0", - "python-socketio==5.11.3", "python-jose==3.3.0", "passlib[bcrypt]==1.7.4", diff --git a/src/lib/components/admin/Settings/Interface.svelte b/src/lib/components/admin/Settings/Interface.svelte index 332d02c5a..316aaad1f 100644 --- a/src/lib/components/admin/Settings/Interface.svelte +++ b/src/lib/components/admin/Settings/Interface.svelte @@ -23,6 +23,7 @@ let taskConfig = { TASK_MODEL: '', TASK_MODEL_EXTERNAL: '', + ENABLE_TITLE_GENERATION: true, TITLE_GENERATION_PROMPT_TEMPLATE: '', IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE: '', ENABLE_AUTOCOMPLETE_GENERATION: true, @@ -126,22 +127,34 @@ -
-
{$i18n.t('Title Generation Prompt')}
+
- -