open-webui/backend/open_webui/utils/plugin.py

271 lines
9.3 KiB
Python
Raw Normal View History

2024-06-11 14:40:27 +08:00
import os
2024-06-24 11:31:40 +08:00
import re
2024-08-08 15:46:14 +08:00
import subprocess
2024-08-28 06:10:27 +08:00
import sys
from importlib import util
2024-09-05 01:55:20 +08:00
import types
2024-09-20 06:12:52 +08:00
import tempfile
import logging
2024-06-11 14:40:27 +08:00
from open_webui.env import SRC_LOG_LEVELS, PIP_OPTIONS, PIP_PACKAGE_INDEX_OPTIONS
2024-12-10 16:54:13 +08:00
from open_webui.models.functions import Functions
from open_webui.models.tools import Tools
2024-06-11 14:40:27 +08:00
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
2024-06-11 14:40:27 +08:00
2024-09-06 02:35:58 +08:00
def extract_frontmatter(content):
2024-06-24 11:31:40 +08:00
"""
2024-09-06 02:35:58 +08:00
Extract frontmatter as a dictionary from the provided content string.
2024-06-24 11:31:40 +08:00
"""
frontmatter = {}
2024-06-24 11:37:41 +08:00
frontmatter_started = False
frontmatter_ended = False
frontmatter_pattern = re.compile(r"^\s*([a-z_]+):\s*(.*)\s*$", re.IGNORECASE)
try:
2024-09-06 02:35:58 +08:00
lines = content.splitlines()
if len(lines) < 1 or lines[0].strip() != '"""':
# The content doesn't start with triple quotes
return {}
frontmatter_started = True
for line in lines[1:]:
if '"""' in line:
if frontmatter_started:
frontmatter_ended = True
break
if frontmatter_started and not frontmatter_ended:
match = frontmatter_pattern.match(line)
if match:
key, value = match.groups()
frontmatter[key.strip()] = value.strip()
2024-06-24 11:37:41 +08:00
except Exception as e:
log.exception(f"Failed to extract frontmatter: {e}")
2024-06-24 11:37:41 +08:00
return {}
2024-06-24 11:31:40 +08:00
return frontmatter
2024-09-05 01:55:20 +08:00
def replace_imports(content):
"""
Replace the import paths in the content.
"""
replacements = {
"from utils": "from open_webui.utils",
"from apps": "from open_webui.apps",
"from main": "from open_webui.main",
"from config": "from open_webui.config",
}
for old, new in replacements.items():
content = content.replace(old, new)
return content
2024-09-05 01:55:20 +08:00
def load_tool_module_by_id(tool_id, content=None):
2024-09-05 01:55:20 +08:00
if content is None:
2025-04-05 19:03:15 +08:00
tool = Tools.get_tool_by_id(tool_id)
2024-09-05 01:55:20 +08:00
if not tool:
2025-04-05 19:03:15 +08:00
raise Exception(f"Toolkit not found: {tool_id}")
2024-09-05 01:55:20 +08:00
content = tool.content
2024-09-05 01:57:41 +08:00
content = replace_imports(content)
2025-04-05 19:03:15 +08:00
Tools.update_tool_by_id(tool_id, {"content": content})
else:
frontmatter = extract_frontmatter(content)
# Install required packages found within the frontmatter
install_frontmatter_requirements(frontmatter.get("requirements", ""))
2024-09-05 01:55:20 +08:00
2025-04-05 19:03:15 +08:00
module_name = f"tool_{tool_id}"
2024-09-05 01:55:20 +08:00
module = types.ModuleType(module_name)
sys.modules[module_name] = module
2024-06-11 14:40:27 +08:00
2024-09-20 06:12:52 +08:00
# Create a temporary file and use it to define `__file__` so
# that it works as expected from the module's perspective.
2024-09-20 06:12:52 +08:00
temp_file = tempfile.NamedTemporaryFile(delete=False)
temp_file.close()
2024-06-11 14:40:27 +08:00
try:
2024-09-20 06:12:52 +08:00
with open(temp_file.name, "w", encoding="utf-8") as f:
f.write(content)
module.__dict__["__file__"] = temp_file.name
2024-09-05 01:55:20 +08:00
# Executing the modified content in the created module's namespace
exec(content, module.__dict__)
frontmatter = extract_frontmatter(content)
log.info(f"Loaded module: {module.__name__}")
2024-09-05 01:55:20 +08:00
# Create and return the object if the class 'Tools' is found in the module
2024-06-11 14:40:27 +08:00
if hasattr(module, "Tools"):
2024-06-24 11:31:40 +08:00
return module.Tools(), frontmatter
2024-06-11 14:40:27 +08:00
else:
2024-09-05 01:55:20 +08:00
raise Exception("No Tools class found in the module")
2024-06-11 14:40:27 +08:00
except Exception as e:
2025-04-05 19:03:15 +08:00
log.error(f"Error loading module: {tool_id}: {e}")
2024-09-05 01:55:20 +08:00
del sys.modules[module_name] # Clean up
2024-06-11 14:40:27 +08:00
raise e
finally:
2024-09-20 06:12:52 +08:00
os.unlink(temp_file.name)
2024-06-20 15:37:02 +08:00
2025-05-27 18:39:35 +08:00
def load_function_module_by_id(function_id: str, content: str | None = None):
2024-09-05 01:55:20 +08:00
if content is None:
function = Functions.get_function_by_id(function_id)
2024-09-05 01:55:20 +08:00
if not function:
raise Exception(f"Function not found: {function_id}")
2024-09-05 01:55:20 +08:00
content = function.content
2024-09-05 01:57:41 +08:00
content = replace_imports(content)
Functions.update_function_by_id(function_id, {"content": content})
else:
frontmatter = extract_frontmatter(content)
install_frontmatter_requirements(frontmatter.get("requirements", ""))
2024-09-05 02:00:47 +08:00
module_name = f"function_{function_id}"
2024-09-05 01:55:20 +08:00
module = types.ModuleType(module_name)
sys.modules[module_name] = module
2024-06-20 15:37:02 +08:00
2024-09-20 06:12:52 +08:00
# Create a temporary file and use it to define `__file__` so
# that it works as expected from the module's perspective.
2024-09-20 06:12:52 +08:00
temp_file = tempfile.NamedTemporaryFile(delete=False)
temp_file.close()
2024-06-20 15:37:02 +08:00
try:
2024-09-20 06:12:52 +08:00
with open(temp_file.name, "w", encoding="utf-8") as f:
f.write(content)
module.__dict__["__file__"] = temp_file.name
2024-09-05 01:55:20 +08:00
# Execute the modified content in the created module's namespace
exec(content, module.__dict__)
frontmatter = extract_frontmatter(content)
log.info(f"Loaded module: {module.__name__}")
2024-09-05 01:55:20 +08:00
# Create appropriate object based on available class type in the module
2024-06-20 15:37:02 +08:00
if hasattr(module, "Pipe"):
2024-06-24 11:31:40 +08:00
return module.Pipe(), "pipe", frontmatter
2024-06-20 15:37:02 +08:00
elif hasattr(module, "Filter"):
2024-06-24 11:31:40 +08:00
return module.Filter(), "filter", frontmatter
2024-07-12 09:41:00 +08:00
elif hasattr(module, "Action"):
return module.Action(), "action", frontmatter
2024-06-20 15:37:02 +08:00
else:
2024-09-05 01:55:20 +08:00
raise Exception("No Function class found in the module")
2024-06-20 15:37:02 +08:00
except Exception as e:
log.error(f"Error loading module: {function_id}: {e}")
# Cleanup by removing the module in case of error
del sys.modules[module_name]
2024-09-05 01:55:20 +08:00
Functions.update_function_by_id(function_id, {"is_active": False})
2024-06-20 15:37:02 +08:00
raise e
finally:
2024-09-20 06:12:52 +08:00
os.unlink(temp_file.name)
2024-08-08 15:46:14 +08:00
2024-08-14 20:49:18 +08:00
2025-05-27 20:06:00 +08:00
def get_function_module_from_cache(request, function_id, load_from_db=True):
if load_from_db:
2025-05-28 05:42:42 +08:00
# Always load from the database by default
# This is useful for hooks like "inlet" or "outlet" where the content might change
# and we want to ensure the latest content is used.
2025-05-27 20:06:00 +08:00
function = Functions.get_function_by_id(function_id)
if not function:
raise Exception(f"Function not found: {function_id}")
content = function.content
new_content = replace_imports(content)
if new_content != content:
content = new_content
# Update the function content in the database
Functions.update_function_by_id(function_id, {"content": content})
if (
hasattr(request.app.state, "FUNCTION_CONTENTS")
and function_id in request.app.state.FUNCTION_CONTENTS
) and (
hasattr(request.app.state, "FUNCTIONS")
and function_id in request.app.state.FUNCTIONS
):
if request.app.state.FUNCTION_CONTENTS[function_id] == content:
return request.app.state.FUNCTIONS[function_id], None, None
function_module, function_type, frontmatter = load_function_module_by_id(
function_id, content
)
else:
# Load from cache (e.g. "stream" hook)
2025-05-28 05:42:42 +08:00
# This is useful for performance reasons
2025-05-27 20:06:00 +08:00
if (
hasattr(request.app.state, "FUNCTIONS")
and function_id in request.app.state.FUNCTIONS
):
2025-05-28 05:38:24 +08:00
return request.app.state.FUNCTIONS[function_id], None, None
2025-05-27 20:06:00 +08:00
function_module, function_type, frontmatter = load_function_module_by_id(
function_id
)
if not hasattr(request.app.state, "FUNCTIONS"):
request.app.state.FUNCTIONS = {}
2025-05-27 18:39:35 +08:00
if not hasattr(request.app.state, "FUNCTION_CONTENTS"):
request.app.state.FUNCTION_CONTENTS = {}
request.app.state.FUNCTIONS[function_id] = function_module
2025-05-27 18:39:35 +08:00
request.app.state.FUNCTION_CONTENTS[function_id] = content
return function_module, function_type, frontmatter
def install_frontmatter_requirements(requirements: str):
2024-08-08 15:46:14 +08:00
if requirements:
try:
req_list = [req.strip() for req in requirements.split(",")]
log.info(f"Installing requirements: {' '.join(req_list)}")
2025-03-18 21:39:42 +08:00
subprocess.check_call(
[sys.executable, "-m", "pip", "install"]
+ PIP_OPTIONS
+ req_list
+ PIP_PACKAGE_INDEX_OPTIONS
)
except Exception as e:
log.error(f"Error installing packages: {' '.join(req_list)}")
raise e
2024-08-08 15:46:14 +08:00
else:
log.info("No requirements found in frontmatter.")
2025-04-30 20:01:23 +08:00
def install_tool_and_function_dependencies():
"""
Install all dependencies for all admin tools and active functions.
By first collecting all dependencies from the frontmatter of each tool and function,
and then installing them using pip. Duplicates or similar version specifications are
handled by pip as much as possible.
"""
function_list = Functions.get_functions(active_only=True)
tool_list = Tools.get_tools()
all_dependencies = ""
try:
for function in function_list:
frontmatter = extract_frontmatter(replace_imports(function.content))
if dependencies := frontmatter.get("requirements"):
all_dependencies += f"{dependencies}, "
for tool in tool_list:
# Only install requirements for admin tools
if tool.user and tool.user.role == "admin":
frontmatter = extract_frontmatter(replace_imports(tool.content))
if dependencies := frontmatter.get("requirements"):
all_dependencies += f"{dependencies}, "
install_frontmatter_requirements(all_dependencies.strip(", "))
except Exception as e:
log.error(f"Error installing requirements: {e}")