mirror of
https://github.com/open-webui/open-webui
synced 2024-12-29 15:25:29 +00:00
316 lines
9.9 KiB
Python
316 lines
9.9 KiB
Python
|
import logging
|
||
|
import sys
|
||
|
import inspect
|
||
|
import json
|
||
|
|
||
|
from pydantic import BaseModel
|
||
|
from typing import AsyncGenerator, Generator, Iterator
|
||
|
from fastapi import (
|
||
|
Depends,
|
||
|
FastAPI,
|
||
|
File,
|
||
|
Form,
|
||
|
HTTPException,
|
||
|
Request,
|
||
|
UploadFile,
|
||
|
status,
|
||
|
)
|
||
|
from starlette.responses import Response, StreamingResponse
|
||
|
|
||
|
|
||
|
from open_webui.socket.main import (
|
||
|
get_event_call,
|
||
|
get_event_emitter,
|
||
|
)
|
||
|
|
||
|
|
||
|
from open_webui.models.functions import Functions
|
||
|
from open_webui.models.models import Models
|
||
|
|
||
|
from open_webui.utils.plugin import load_function_module_by_id
|
||
|
from open_webui.utils.tools import get_tools
|
||
|
from open_webui.utils.access_control import has_access
|
||
|
|
||
|
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
|
||
|
|
||
|
from open_webui.utils.misc import (
|
||
|
add_or_update_system_message,
|
||
|
get_last_user_message,
|
||
|
prepend_to_first_user_message_content,
|
||
|
openai_chat_chunk_message_template,
|
||
|
openai_chat_completion_message_template,
|
||
|
)
|
||
|
from open_webui.utils.payload import (
|
||
|
apply_model_params_to_body_openai,
|
||
|
apply_model_system_prompt_to_body,
|
||
|
)
|
||
|
|
||
|
|
||
|
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
|
||
|
log = logging.getLogger(__name__)
|
||
|
log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
||
|
|
||
|
|
||
|
def get_function_module_by_id(request: Request, pipe_id: str):
|
||
|
# Check if function is already loaded
|
||
|
if pipe_id not in request.app.state.FUNCTIONS:
|
||
|
function_module, _, _ = load_function_module_by_id(pipe_id)
|
||
|
request.app.state.FUNCTIONS[pipe_id] = function_module
|
||
|
else:
|
||
|
function_module = request.app.state.FUNCTIONS[pipe_id]
|
||
|
|
||
|
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
|
||
|
valves = Functions.get_function_valves_by_id(pipe_id)
|
||
|
function_module.valves = function_module.Valves(**(valves if valves else {}))
|
||
|
return function_module
|
||
|
|
||
|
|
||
|
async def get_function_models():
|
||
|
pipes = Functions.get_functions_by_type("pipe", active_only=True)
|
||
|
pipe_models = []
|
||
|
|
||
|
for pipe in pipes:
|
||
|
function_module = get_function_module_by_id(pipe.id)
|
||
|
|
||
|
# Check if function is a manifold
|
||
|
if hasattr(function_module, "pipes"):
|
||
|
sub_pipes = []
|
||
|
|
||
|
# Check if pipes is a function or a list
|
||
|
|
||
|
try:
|
||
|
if callable(function_module.pipes):
|
||
|
sub_pipes = function_module.pipes()
|
||
|
else:
|
||
|
sub_pipes = function_module.pipes
|
||
|
except Exception as e:
|
||
|
log.exception(e)
|
||
|
sub_pipes = []
|
||
|
|
||
|
log.debug(
|
||
|
f"get_function_models: function '{pipe.id}' is a manifold of {sub_pipes}"
|
||
|
)
|
||
|
|
||
|
for p in sub_pipes:
|
||
|
sub_pipe_id = f'{pipe.id}.{p["id"]}'
|
||
|
sub_pipe_name = p["name"]
|
||
|
|
||
|
if hasattr(function_module, "name"):
|
||
|
sub_pipe_name = f"{function_module.name}{sub_pipe_name}"
|
||
|
|
||
|
pipe_flag = {"type": pipe.type}
|
||
|
|
||
|
pipe_models.append(
|
||
|
{
|
||
|
"id": sub_pipe_id,
|
||
|
"name": sub_pipe_name,
|
||
|
"object": "model",
|
||
|
"created": pipe.created_at,
|
||
|
"owned_by": "openai",
|
||
|
"pipe": pipe_flag,
|
||
|
}
|
||
|
)
|
||
|
else:
|
||
|
pipe_flag = {"type": "pipe"}
|
||
|
|
||
|
log.debug(
|
||
|
f"get_function_models: function '{pipe.id}' is a single pipe {{ 'id': {pipe.id}, 'name': {pipe.name} }}"
|
||
|
)
|
||
|
|
||
|
pipe_models.append(
|
||
|
{
|
||
|
"id": pipe.id,
|
||
|
"name": pipe.name,
|
||
|
"object": "model",
|
||
|
"created": pipe.created_at,
|
||
|
"owned_by": "openai",
|
||
|
"pipe": pipe_flag,
|
||
|
}
|
||
|
)
|
||
|
|
||
|
return pipe_models
|
||
|
|
||
|
|
||
|
async def generate_function_chat_completion(
|
||
|
request, form_data, user, models: dict = {}
|
||
|
):
|
||
|
async def execute_pipe(pipe, params):
|
||
|
if inspect.iscoroutinefunction(pipe):
|
||
|
return await pipe(**params)
|
||
|
else:
|
||
|
return pipe(**params)
|
||
|
|
||
|
async def get_message_content(res: str | Generator | AsyncGenerator) -> str:
|
||
|
if isinstance(res, str):
|
||
|
return res
|
||
|
if isinstance(res, Generator):
|
||
|
return "".join(map(str, res))
|
||
|
if isinstance(res, AsyncGenerator):
|
||
|
return "".join([str(stream) async for stream in res])
|
||
|
|
||
|
def process_line(form_data: dict, line):
|
||
|
if isinstance(line, BaseModel):
|
||
|
line = line.model_dump_json()
|
||
|
line = f"data: {line}"
|
||
|
if isinstance(line, dict):
|
||
|
line = f"data: {json.dumps(line)}"
|
||
|
|
||
|
try:
|
||
|
line = line.decode("utf-8")
|
||
|
except Exception:
|
||
|
pass
|
||
|
|
||
|
if line.startswith("data:"):
|
||
|
return f"{line}\n\n"
|
||
|
else:
|
||
|
line = openai_chat_chunk_message_template(form_data["model"], line)
|
||
|
return f"data: {json.dumps(line)}\n\n"
|
||
|
|
||
|
def get_pipe_id(form_data: dict) -> str:
|
||
|
pipe_id = form_data["model"]
|
||
|
if "." in pipe_id:
|
||
|
pipe_id, _ = pipe_id.split(".", 1)
|
||
|
return pipe_id
|
||
|
|
||
|
def get_function_params(function_module, form_data, user, extra_params=None):
|
||
|
if extra_params is None:
|
||
|
extra_params = {}
|
||
|
|
||
|
pipe_id = get_pipe_id(form_data)
|
||
|
|
||
|
# Get the signature of the function
|
||
|
sig = inspect.signature(function_module.pipe)
|
||
|
params = {"body": form_data} | {
|
||
|
k: v for k, v in extra_params.items() if k in sig.parameters
|
||
|
}
|
||
|
|
||
|
if "__user__" in params and hasattr(function_module, "UserValves"):
|
||
|
user_valves = Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id)
|
||
|
try:
|
||
|
params["__user__"]["valves"] = function_module.UserValves(**user_valves)
|
||
|
except Exception as e:
|
||
|
log.exception(e)
|
||
|
params["__user__"]["valves"] = function_module.UserValves()
|
||
|
|
||
|
return params
|
||
|
|
||
|
model_id = form_data.get("model")
|
||
|
model_info = Models.get_model_by_id(model_id)
|
||
|
|
||
|
metadata = form_data.pop("metadata", {})
|
||
|
|
||
|
files = metadata.get("files", [])
|
||
|
tool_ids = metadata.get("tool_ids", [])
|
||
|
# Check if tool_ids is None
|
||
|
if tool_ids is None:
|
||
|
tool_ids = []
|
||
|
|
||
|
__event_emitter__ = None
|
||
|
__event_call__ = None
|
||
|
__task__ = None
|
||
|
__task_body__ = None
|
||
|
|
||
|
if metadata:
|
||
|
if all(k in metadata for k in ("session_id", "chat_id", "message_id")):
|
||
|
__event_emitter__ = get_event_emitter(metadata)
|
||
|
__event_call__ = get_event_call(metadata)
|
||
|
__task__ = metadata.get("task", None)
|
||
|
__task_body__ = metadata.get("task_body", None)
|
||
|
|
||
|
extra_params = {
|
||
|
"__event_emitter__": __event_emitter__,
|
||
|
"__event_call__": __event_call__,
|
||
|
"__task__": __task__,
|
||
|
"__task_body__": __task_body__,
|
||
|
"__files__": files,
|
||
|
"__user__": {
|
||
|
"id": user.id,
|
||
|
"email": user.email,
|
||
|
"name": user.name,
|
||
|
"role": user.role,
|
||
|
},
|
||
|
"__metadata__": metadata,
|
||
|
}
|
||
|
extra_params["__tools__"] = get_tools(
|
||
|
request,
|
||
|
tool_ids,
|
||
|
user,
|
||
|
{
|
||
|
**extra_params,
|
||
|
"__model__": models.get(form_data["model"], None),
|
||
|
"__messages__": form_data["messages"],
|
||
|
"__files__": files,
|
||
|
},
|
||
|
)
|
||
|
|
||
|
if model_info:
|
||
|
if model_info.base_model_id:
|
||
|
form_data["model"] = model_info.base_model_id
|
||
|
|
||
|
params = model_info.params.model_dump()
|
||
|
form_data = apply_model_params_to_body_openai(params, form_data)
|
||
|
form_data = apply_model_system_prompt_to_body(params, form_data, user)
|
||
|
|
||
|
pipe_id = get_pipe_id(form_data)
|
||
|
function_module = get_function_module_by_id(pipe_id)
|
||
|
|
||
|
pipe = function_module.pipe
|
||
|
params = get_function_params(function_module, form_data, user, extra_params)
|
||
|
|
||
|
if form_data.get("stream", False):
|
||
|
|
||
|
async def stream_content():
|
||
|
try:
|
||
|
res = await execute_pipe(pipe, params)
|
||
|
|
||
|
# Directly return if the response is a StreamingResponse
|
||
|
if isinstance(res, StreamingResponse):
|
||
|
async for data in res.body_iterator:
|
||
|
yield data
|
||
|
return
|
||
|
if isinstance(res, dict):
|
||
|
yield f"data: {json.dumps(res)}\n\n"
|
||
|
return
|
||
|
|
||
|
except Exception as e:
|
||
|
log.error(f"Error: {e}")
|
||
|
yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n"
|
||
|
return
|
||
|
|
||
|
if isinstance(res, str):
|
||
|
message = openai_chat_chunk_message_template(form_data["model"], res)
|
||
|
yield f"data: {json.dumps(message)}\n\n"
|
||
|
|
||
|
if isinstance(res, Iterator):
|
||
|
for line in res:
|
||
|
yield process_line(form_data, line)
|
||
|
|
||
|
if isinstance(res, AsyncGenerator):
|
||
|
async for line in res:
|
||
|
yield process_line(form_data, line)
|
||
|
|
||
|
if isinstance(res, str) or isinstance(res, Generator):
|
||
|
finish_message = openai_chat_chunk_message_template(
|
||
|
form_data["model"], ""
|
||
|
)
|
||
|
finish_message["choices"][0]["finish_reason"] = "stop"
|
||
|
yield f"data: {json.dumps(finish_message)}\n\n"
|
||
|
yield "data: [DONE]"
|
||
|
|
||
|
return StreamingResponse(stream_content(), media_type="text/event-stream")
|
||
|
else:
|
||
|
try:
|
||
|
res = await execute_pipe(pipe, params)
|
||
|
|
||
|
except Exception as e:
|
||
|
log.error(f"Error: {e}")
|
||
|
return {"error": {"detail": str(e)}}
|
||
|
|
||
|
if isinstance(res, StreamingResponse) or isinstance(res, dict):
|
||
|
return res
|
||
|
if isinstance(res, BaseModel):
|
||
|
return res.model_dump()
|
||
|
|
||
|
message = await get_message_content(res)
|
||
|
return openai_chat_completion_message_template(form_data["model"], message)
|