Skip to content

Commit 2017856

Browse files
authored
Merge pull request open-webui#9945 from open-webui/dev
0.5.12
2 parents e4d7d41 + 893bfe9 commit 2017856

File tree

77 files changed

+869
-141
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+869
-141
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.5.12] - 2025-02-13
9+
10+
### Added
11+
12+
- **🛠️ 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.
13+
14+
### Fixed
15+
16+
- **📝 Playground Text Completion Restored**: Addressed an issue where text completion in the Playground was not functioning.
17+
- **🔗 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.
18+
- **⚡ 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.
19+
- **🔧 Parameter in Functions Fixed**: Fixed an issue where the reserved parameters wasn’t recognized within functions, restoring full functionality for advanced task-based automation.
20+
821
## [0.5.11] - 2025-02-13
922

1023
### Added

backend/open_webui/config.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,6 +1190,12 @@ class BannerModel(BaseModel):
11901190
os.environ.get("ENABLE_TAGS_GENERATION", "True").lower() == "true",
11911191
)
11921192

1193+
ENABLE_TITLE_GENERATION = PersistentConfig(
1194+
"ENABLE_TITLE_GENERATION",
1195+
"task.title.enable",
1196+
os.environ.get("ENABLE_TITLE_GENERATION", "True").lower() == "true",
1197+
)
1198+
11931199

11941200
ENABLE_SEARCH_QUERY_GENERATION = PersistentConfig(
11951201
"ENABLE_SEARCH_QUERY_GENERATION",
@@ -1803,6 +1809,18 @@ class BannerModel(BaseModel):
18031809
os.getenv("SEARCHAPI_ENGINE", ""),
18041810
)
18051811

1812+
SERPAPI_API_KEY = PersistentConfig(
1813+
"SERPAPI_API_KEY",
1814+
"rag.web.search.serpapi_api_key",
1815+
os.getenv("SERPAPI_API_KEY", ""),
1816+
)
1817+
1818+
SERPAPI_ENGINE = PersistentConfig(
1819+
"SERPAPI_ENGINE",
1820+
"rag.web.search.serpapi_engine",
1821+
os.getenv("SERPAPI_ENGINE", ""),
1822+
)
1823+
18061824
BING_SEARCH_V7_ENDPOINT = PersistentConfig(
18071825
"BING_SEARCH_V7_ENDPOINT",
18081826
"rag.web.search.bing_search_v7_endpoint",

backend/open_webui/main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@
179179
JINA_API_KEY,
180180
SEARCHAPI_API_KEY,
181181
SEARCHAPI_ENGINE,
182+
SERPAPI_API_KEY,
183+
SERPAPI_ENGINE,
182184
SEARXNG_QUERY_URL,
183185
SERPER_API_KEY,
184186
SERPLY_API_KEY,
@@ -264,6 +266,7 @@
264266
TASK_MODEL,
265267
TASK_MODEL_EXTERNAL,
266268
ENABLE_TAGS_GENERATION,
269+
ENABLE_TITLE_GENERATION,
267270
ENABLE_SEARCH_QUERY_GENERATION,
268271
ENABLE_RETRIEVAL_QUERY_GENERATION,
269272
ENABLE_AUTOCOMPLETE_GENERATION,
@@ -546,6 +549,8 @@ async def lifespan(app: FastAPI):
546549
app.state.config.TAVILY_API_KEY = TAVILY_API_KEY
547550
app.state.config.SEARCHAPI_API_KEY = SEARCHAPI_API_KEY
548551
app.state.config.SEARCHAPI_ENGINE = SEARCHAPI_ENGINE
552+
app.state.config.SERPAPI_API_KEY = SERPAPI_API_KEY
553+
app.state.config.SERPAPI_ENGINE = SERPAPI_ENGINE
549554
app.state.config.JINA_API_KEY = JINA_API_KEY
550555
app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT
551556
app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY
@@ -689,6 +694,7 @@ async def lifespan(app: FastAPI):
689694
app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION = ENABLE_RETRIEVAL_QUERY_GENERATION
690695
app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = ENABLE_AUTOCOMPLETE_GENERATION
691696
app.state.config.ENABLE_TAGS_GENERATION = ENABLE_TAGS_GENERATION
697+
app.state.config.ENABLE_TITLE_GENERATION = ENABLE_TITLE_GENERATION
692698

693699

694700
app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import logging
2+
from typing import Optional
3+
from urllib.parse import urlencode
4+
5+
import requests
6+
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
7+
from open_webui.env import SRC_LOG_LEVELS
8+
9+
log = logging.getLogger(__name__)
10+
log.setLevel(SRC_LOG_LEVELS["RAG"])
11+
12+
13+
def search_serpapi(
14+
api_key: str,
15+
engine: str,
16+
query: str,
17+
count: int,
18+
filter_list: Optional[list[str]] = None,
19+
) -> list[SearchResult]:
20+
"""Search using serpapi.com's API and return the results as a list of SearchResult objects.
21+
22+
Args:
23+
api_key (str): A serpapi.com API key
24+
query (str): The query to search for
25+
"""
26+
url = "https://serpapi.com/search"
27+
28+
engine = engine or "google"
29+
30+
payload = {"engine": engine, "q": query, "api_key": api_key}
31+
32+
url = f"{url}?{urlencode(payload)}"
33+
response = requests.request("GET", url)
34+
35+
json_response = response.json()
36+
log.info(f"results from serpapi search: {json_response}")
37+
38+
results = sorted(
39+
json_response.get("organic_results", []), key=lambda x: x.get("position", 0)
40+
)
41+
if filter_list:
42+
results = get_filtered_results(results, filter_list)
43+
return [
44+
SearchResult(
45+
link=result["link"], title=result["title"], snippet=result["snippet"]
46+
)
47+
for result in results[:count]
48+
]

backend/open_webui/routers/retrieval.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from open_webui.retrieval.web.google_pse import search_google_pse
5151
from open_webui.retrieval.web.jina_search import search_jina
5252
from open_webui.retrieval.web.searchapi import search_searchapi
53+
from open_webui.retrieval.web.serpapi import search_serpapi
5354
from open_webui.retrieval.web.searxng import search_searxng
5455
from open_webui.retrieval.web.serper import search_serper
5556
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)):
388389
"tavily_api_key": request.app.state.config.TAVILY_API_KEY,
389390
"searchapi_api_key": request.app.state.config.SEARCHAPI_API_KEY,
390391
"searchapi_engine": request.app.state.config.SEARCHAPI_ENGINE,
392+
"serpapi_api_key": request.app.state.config.SERPAPI_API_KEY,
393+
"serpapi_engine": request.app.state.config.SERPAPI_ENGINE,
391394
"jina_api_key": request.app.state.config.JINA_API_KEY,
392395
"bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
393396
"bing_search_v7_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY,
@@ -439,6 +442,8 @@ class WebSearchConfig(BaseModel):
439442
tavily_api_key: Optional[str] = None
440443
searchapi_api_key: Optional[str] = None
441444
searchapi_engine: Optional[str] = None
445+
serpapi_api_key: Optional[str] = None
446+
serpapi_engine: Optional[str] = None
442447
jina_api_key: Optional[str] = None
443448
bing_search_v7_endpoint: Optional[str] = None
444449
bing_search_v7_subscription_key: Optional[str] = None
@@ -545,6 +550,9 @@ async def update_rag_config(
545550
form_data.web.search.searchapi_engine
546551
)
547552

553+
request.app.state.config.SERPAPI_API_KEY = form_data.web.search.serpapi_api_key
554+
request.app.state.config.SERPAPI_ENGINE = form_data.web.search.serpapi_engine
555+
548556
request.app.state.config.JINA_API_KEY = form_data.web.search.jina_api_key
549557
request.app.state.config.BING_SEARCH_V7_ENDPOINT = (
550558
form_data.web.search.bing_search_v7_endpoint
@@ -604,6 +612,8 @@ async def update_rag_config(
604612
"serply_api_key": request.app.state.config.SERPLY_API_KEY,
605613
"serachapi_api_key": request.app.state.config.SEARCHAPI_API_KEY,
606614
"searchapi_engine": request.app.state.config.SEARCHAPI_ENGINE,
615+
"serpapi_api_key": request.app.state.config.SERPAPI_API_KEY,
616+
"serpapi_engine": request.app.state.config.SERPAPI_ENGINE,
607617
"tavily_api_key": request.app.state.config.TAVILY_API_KEY,
608618
"jina_api_key": request.app.state.config.JINA_API_KEY,
609619
"bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
@@ -760,7 +770,11 @@ def _get_docs_info(docs: list[Document]) -> str:
760770
# for meta-data so convert them to string.
761771
for metadata in metadatas:
762772
for key, value in metadata.items():
763-
if isinstance(value, datetime):
773+
if (
774+
isinstance(value, datetime)
775+
or isinstance(value, list)
776+
or isinstance(value, dict)
777+
):
764778
metadata[key] = str(value)
765779

766780
try:
@@ -1127,6 +1141,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
11271141
- TAVILY_API_KEY
11281142
- EXA_API_KEY
11291143
- SEARCHAPI_API_KEY + SEARCHAPI_ENGINE (by default `google`)
1144+
- SERPAPI_API_KEY + SERPAPI_ENGINE (by default `google`)
11301145
Args:
11311146
query (str): The query to search for
11321147
"""
@@ -1255,6 +1270,17 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
12551270
)
12561271
else:
12571272
raise Exception("No SEARCHAPI_API_KEY found in environment variables")
1273+
elif engine == "serpapi":
1274+
if request.app.state.config.SERPAPI_API_KEY:
1275+
return search_serpapi(
1276+
request.app.state.config.SERPAPI_API_KEY,
1277+
request.app.state.config.SERPAPI_ENGINE,
1278+
query,
1279+
request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
1280+
request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
1281+
)
1282+
else:
1283+
raise Exception("No SERPAPI_API_KEY found in environment variables")
12581284
elif engine == "jina":
12591285
return search_jina(
12601286
request.app.state.config.JINA_API_KEY,

backend/open_webui/routers/tasks.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ async def get_task_config(request: Request, user=Depends(get_verified_user)):
5858
"AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH": request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
5959
"TAGS_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
6060
"ENABLE_TAGS_GENERATION": request.app.state.config.ENABLE_TAGS_GENERATION,
61+
"ENABLE_TITLE_GENERATION": request.app.state.config.ENABLE_TITLE_GENERATION,
6162
"ENABLE_SEARCH_QUERY_GENERATION": request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION,
6263
"ENABLE_RETRIEVAL_QUERY_GENERATION": request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION,
6364
"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)):
6869
class TaskConfigForm(BaseModel):
6970
TASK_MODEL: Optional[str]
7071
TASK_MODEL_EXTERNAL: Optional[str]
72+
ENABLE_TITLE_GENERATION: bool
7173
TITLE_GENERATION_PROMPT_TEMPLATE: str
7274
IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE: str
7375
ENABLE_AUTOCOMPLETE_GENERATION: bool
@@ -86,6 +88,7 @@ async def update_task_config(
8688
):
8789
request.app.state.config.TASK_MODEL = form_data.TASK_MODEL
8890
request.app.state.config.TASK_MODEL_EXTERNAL = form_data.TASK_MODEL_EXTERNAL
91+
request.app.state.config.ENABLE_TITLE_GENERATION = form_data.ENABLE_TITLE_GENERATION
8992
request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = (
9093
form_data.TITLE_GENERATION_PROMPT_TEMPLATE
9194
)
@@ -122,6 +125,7 @@ async def update_task_config(
122125
return {
123126
"TASK_MODEL": request.app.state.config.TASK_MODEL,
124127
"TASK_MODEL_EXTERNAL": request.app.state.config.TASK_MODEL_EXTERNAL,
128+
"ENABLE_TITLE_GENERATION": request.app.state.config.ENABLE_TITLE_GENERATION,
125129
"TITLE_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
126130
"IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE": request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
127131
"ENABLE_AUTOCOMPLETE_GENERATION": request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
@@ -139,6 +143,13 @@ async def update_task_config(
139143
async def generate_title(
140144
request: Request, form_data: dict, user=Depends(get_verified_user)
141145
):
146+
147+
if not request.app.state.config.ENABLE_TITLE_GENERATION:
148+
return JSONResponse(
149+
status_code=status.HTTP_200_OK,
150+
content={"detail": "Title generation is disabled"},
151+
)
152+
142153
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
143154
models = {
144155
request.state.model["id"]: request.state.model,

backend/open_webui/utils/chat.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,18 @@ async def generate_chat_completion(
161161
user: Any,
162162
bypass_filter: bool = False,
163163
):
164+
log.debug(f"generate_chat_completion: {form_data}")
164165
if BYPASS_MODEL_ACCESS_CONTROL:
165166
bypass_filter = True
166167

167168
if hasattr(request.state, "metadata"):
168-
form_data["metadata"] = request.state.metadata
169+
if "metadata" not in form_data:
170+
form_data["metadata"] = request.state.metadata
171+
else:
172+
form_data["metadata"] = {
173+
**form_data["metadata"],
174+
**request.state.metadata,
175+
}
169176

170177
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
171178
models = {
@@ -187,19 +194,18 @@ async def generate_chat_completion(
187194

188195
model = models[model_id]
189196

190-
# Check if user has access to the model
191-
if not bypass_filter and user.role == "user":
192-
try:
193-
check_model_access(user, model)
194-
except Exception as e:
195-
raise e
196-
197197
if getattr(request.state, "direct", False):
198198
return await generate_direct_chat_completion(
199199
request, form_data, user=user, models=models
200200
)
201-
202201
else:
202+
# Check if user has access to the model
203+
if not bypass_filter and user.role == "user":
204+
try:
205+
check_model_access(user, model)
206+
except Exception as e:
207+
raise e
208+
203209
if model["owned_by"] == "arena":
204210
model_ids = model.get("info", {}).get("meta", {}).get("model_ids")
205211
filter_mode = model.get("info", {}).get("meta", {}).get("filter_mode")

backend/open_webui/utils/middleware.py

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,46 @@ def serialize_content_blocks(content_blocks, raw=False):
11511151

11521152
return content.strip()
11531153

1154+
def convert_content_blocks_to_messages(content_blocks):
1155+
messages = []
1156+
1157+
temp_blocks = []
1158+
for idx, block in enumerate(content_blocks):
1159+
if block["type"] == "tool_calls":
1160+
messages.append(
1161+
{
1162+
"role": "assistant",
1163+
"content": serialize_content_blocks(temp_blocks),
1164+
"tool_calls": block.get("content"),
1165+
}
1166+
)
1167+
1168+
results = block.get("results", [])
1169+
1170+
for result in results:
1171+
messages.append(
1172+
{
1173+
"role": "tool",
1174+
"tool_call_id": result["tool_call_id"],
1175+
"content": result["content"],
1176+
}
1177+
)
1178+
temp_blocks = []
1179+
else:
1180+
temp_blocks.append(block)
1181+
1182+
if temp_blocks:
1183+
content = serialize_content_blocks(temp_blocks)
1184+
if content:
1185+
messages.append(
1186+
{
1187+
"role": "assistant",
1188+
"content": content,
1189+
}
1190+
)
1191+
1192+
return messages
1193+
11541194
def tag_content_handler(content_type, tags, content, content_blocks):
11551195
end_flag = False
11561196

@@ -1542,7 +1582,6 @@ async def stream_body_handler(response):
15421582

15431583
results = []
15441584
for tool_call in response_tool_calls:
1545-
print("\n\n" + str(tool_call) + "\n\n")
15461585
tool_call_id = tool_call.get("id", "")
15471586
tool_name = tool_call.get("function", {}).get("name", "")
15481587

@@ -1608,23 +1647,10 @@ async def stream_body_handler(response):
16081647
{
16091648
"model": model_id,
16101649
"stream": True,
1650+
"tools": form_data["tools"],
16111651
"messages": [
16121652
*form_data["messages"],
1613-
{
1614-
"role": "assistant",
1615-
"content": serialize_content_blocks(
1616-
content_blocks, raw=True
1617-
),
1618-
"tool_calls": response_tool_calls,
1619-
},
1620-
*[
1621-
{
1622-
"role": "tool",
1623-
"tool_call_id": result["tool_call_id"],
1624-
"content": result["content"],
1625-
}
1626-
for result in results
1627-
],
1653+
*convert_content_blocks_to_messages(content_blocks),
16281654
],
16291655
},
16301656
user,

0 commit comments

Comments
 (0)