diff --git a/backend/apps/webui/main.py b/backend/apps/webui/main.py index a8f45aff0..f092da7ad 100644 --- a/backend/apps/webui/main.py +++ b/backend/apps/webui/main.py @@ -109,7 +109,9 @@ async def get_pipe_models(): for pipe in pipes: # Check if function is already loaded if pipe.id not in app.state.FUNCTIONS: - function_module, function_type = load_function_module_by_id(pipe.id) + function_module, function_type, frontmatter = load_function_module_by_id( + pipe.id + ) app.state.FUNCTIONS[pipe.id] = function_module else: function_module = app.state.FUNCTIONS[pipe.id] diff --git a/backend/apps/webui/models/functions.py b/backend/apps/webui/models/functions.py index f4366a1a7..261987981 100644 --- a/backend/apps/webui/models/functions.py +++ b/backend/apps/webui/models/functions.py @@ -39,6 +39,7 @@ class Function(Model): class FunctionMeta(BaseModel): description: Optional[str] = None + manifest: Optional[dict] = {} class FunctionModel(BaseModel): diff --git a/backend/apps/webui/models/tools.py b/backend/apps/webui/models/tools.py index bfa65742b..694081df9 100644 --- a/backend/apps/webui/models/tools.py +++ b/backend/apps/webui/models/tools.py @@ -38,6 +38,7 @@ class Tool(Model): class ToolMeta(BaseModel): description: Optional[str] = None + manifest: Optional[dict] = {} class ToolModel(BaseModel): diff --git a/backend/apps/webui/routers/functions.py b/backend/apps/webui/routers/functions.py index fa3e3aeb9..4c89ca487 100644 --- a/backend/apps/webui/routers/functions.py +++ b/backend/apps/webui/routers/functions.py @@ -69,7 +69,10 @@ async def create_new_function( with open(function_path, "w") as function_file: function_file.write(form_data.content) - function_module, function_type = load_function_module_by_id(form_data.id) + function_module, function_type, frontmatter = load_function_module_by_id( + form_data.id + ) + form_data.meta.manifest = frontmatter FUNCTIONS = request.app.state.FUNCTIONS FUNCTIONS[form_data.id] = function_module @@ -117,6 +120,97 @@ async def get_function_by_id(id: str, user=Depends(get_admin_user)): ) +############################ +# ToggleFunctionById +############################ + + +@router.post("/id/{id}/toggle", response_model=Optional[FunctionModel]) +async def toggle_function_by_id(id: str, user=Depends(get_admin_user)): + function = Functions.get_function_by_id(id) + if function: + function = Functions.update_function_by_id( + id, {"is_active": not function.is_active} + ) + + if function: + return function + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Error updating function"), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# UpdateFunctionById +############################ + + +@router.post("/id/{id}/update", response_model=Optional[FunctionModel]) +async def update_function_by_id( + request: Request, id: str, form_data: FunctionForm, user=Depends(get_admin_user) +): + function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py") + + try: + with open(function_path, "w") as function_file: + function_file.write(form_data.content) + + function_module, function_type, frontmatter = load_function_module_by_id(id) + form_data.meta.manifest = frontmatter + + FUNCTIONS = request.app.state.FUNCTIONS + FUNCTIONS[id] = function_module + + updated = {**form_data.model_dump(exclude={"id"}), "type": function_type} + print(updated) + + function = Functions.update_function_by_id(id, updated) + + if function: + return function + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Error updating function"), + ) + + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +############################ +# DeleteFunctionById +############################ + + +@router.delete("/id/{id}/delete", response_model=bool) +async def delete_function_by_id( + request: Request, id: str, user=Depends(get_admin_user) +): + result = Functions.delete_function_by_id(id) + + if result: + FUNCTIONS = request.app.state.FUNCTIONS + if id in FUNCTIONS: + del FUNCTIONS[id] + + # delete the function file + function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py") + os.remove(function_path) + + return result + + ############################ # GetFunctionValves ############################ @@ -155,7 +249,7 @@ async def get_function_valves_spec_by_id( if id in request.app.state.FUNCTIONS: function_module = request.app.state.FUNCTIONS[id] else: - function_module, function_type = load_function_module_by_id(id) + function_module, function_type, frontmatter = load_function_module_by_id(id) request.app.state.FUNCTIONS[id] = function_module if hasattr(function_module, "Valves"): @@ -184,7 +278,7 @@ async def update_function_valves_by_id( if id in request.app.state.FUNCTIONS: function_module = request.app.state.FUNCTIONS[id] else: - function_module, function_type = load_function_module_by_id(id) + function_module, function_type, frontmatter = load_function_module_by_id(id) request.app.state.FUNCTIONS[id] = function_module if hasattr(function_module, "Valves"): @@ -247,7 +341,7 @@ async def get_function_user_valves_spec_by_id( if id in request.app.state.FUNCTIONS: function_module = request.app.state.FUNCTIONS[id] else: - function_module, function_type = load_function_module_by_id(id) + function_module, function_type, frontmatter = load_function_module_by_id(id) request.app.state.FUNCTIONS[id] = function_module if hasattr(function_module, "UserValves"): @@ -271,7 +365,7 @@ async def update_function_user_valves_by_id( if id in request.app.state.FUNCTIONS: function_module = request.app.state.FUNCTIONS[id] else: - function_module, function_type = load_function_module_by_id(id) + function_module, function_type, frontmatter = load_function_module_by_id(id) request.app.state.FUNCTIONS[id] = function_module if hasattr(function_module, "UserValves"): @@ -300,93 +394,3 @@ async def update_function_user_valves_by_id( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND, ) - - -############################ -# ToggleFunctionById -############################ - - -@router.post("/id/{id}/toggle", response_model=Optional[FunctionModel]) -async def toggle_function_by_id(id: str, user=Depends(get_admin_user)): - function = Functions.get_function_by_id(id) - if function: - function = Functions.update_function_by_id( - id, {"is_active": not function.is_active} - ) - - if function: - return function - else: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error updating function"), - ) - else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.NOT_FOUND, - ) - - -############################ -# UpdateFunctionById -############################ - - -@router.post("/id/{id}/update", response_model=Optional[FunctionModel]) -async def update_function_by_id( - request: Request, id: str, form_data: FunctionForm, user=Depends(get_admin_user) -): - function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py") - - try: - with open(function_path, "w") as function_file: - function_file.write(form_data.content) - - function_module, function_type = load_function_module_by_id(id) - - FUNCTIONS = request.app.state.FUNCTIONS - FUNCTIONS[id] = function_module - - updated = {**form_data.model_dump(exclude={"id"}), "type": function_type} - print(updated) - - function = Functions.update_function_by_id(id, updated) - - if function: - return function - else: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error updating function"), - ) - - except Exception as e: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT(e), - ) - - -############################ -# DeleteFunctionById -############################ - - -@router.delete("/id/{id}/delete", response_model=bool) -async def delete_function_by_id( - request: Request, id: str, user=Depends(get_admin_user) -): - result = Functions.delete_function_by_id(id) - - if result: - FUNCTIONS = request.app.state.FUNCTIONS - if id in FUNCTIONS: - del FUNCTIONS[id] - - # delete the function file - function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py") - os.remove(function_path) - - return result diff --git a/backend/apps/webui/routers/tools.py b/backend/apps/webui/routers/tools.py index ab974391c..d20584c22 100644 --- a/backend/apps/webui/routers/tools.py +++ b/backend/apps/webui/routers/tools.py @@ -74,7 +74,8 @@ async def create_new_toolkit( with open(toolkit_path, "w") as tool_file: tool_file.write(form_data.content) - toolkit_module = load_toolkit_module_by_id(form_data.id) + toolkit_module, frontmatter = load_toolkit_module_by_id(form_data.id) + form_data.meta.manifest = frontmatter TOOLS = request.app.state.TOOLS TOOLS[form_data.id] = toolkit_module @@ -123,6 +124,73 @@ async def get_toolkit_by_id(id: str, user=Depends(get_admin_user)): ) +############################ +# UpdateToolkitById +############################ + + +@router.post("/id/{id}/update", response_model=Optional[ToolModel]) +async def update_toolkit_by_id( + request: Request, id: str, form_data: ToolForm, user=Depends(get_admin_user) +): + toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py") + + try: + with open(toolkit_path, "w") as tool_file: + tool_file.write(form_data.content) + + toolkit_module, frontmatter = load_toolkit_module_by_id(id) + form_data.meta.manifest = frontmatter + + TOOLS = request.app.state.TOOLS + TOOLS[id] = toolkit_module + + specs = get_tools_specs(TOOLS[id]) + + updated = { + **form_data.model_dump(exclude={"id"}), + "specs": specs, + } + + print(updated) + toolkit = Tools.update_tool_by_id(id, updated) + + if toolkit: + return toolkit + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Error updating toolkit"), + ) + + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +############################ +# DeleteToolkitById +############################ + + +@router.delete("/id/{id}/delete", response_model=bool) +async def delete_toolkit_by_id(request: Request, id: str, user=Depends(get_admin_user)): + result = Tools.delete_tool_by_id(id) + + if result: + TOOLS = request.app.state.TOOLS + if id in TOOLS: + del TOOLS[id] + + # delete the toolkit file + toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py") + os.remove(toolkit_path) + + return result + + ############################ # GetToolValves ############################ @@ -161,7 +229,7 @@ async def get_toolkit_valves_spec_by_id( if id in request.app.state.TOOLS: toolkit_module = request.app.state.TOOLS[id] else: - toolkit_module = load_toolkit_module_by_id(id) + toolkit_module, frontmatter = load_toolkit_module_by_id(id) request.app.state.TOOLS[id] = toolkit_module if hasattr(toolkit_module, "Valves"): @@ -189,7 +257,7 @@ async def update_toolkit_valves_by_id( if id in request.app.state.TOOLS: toolkit_module = request.app.state.TOOLS[id] else: - toolkit_module = load_toolkit_module_by_id(id) + toolkit_module, frontmatter = load_toolkit_module_by_id(id) request.app.state.TOOLS[id] = toolkit_module if hasattr(toolkit_module, "Valves"): @@ -252,7 +320,7 @@ async def get_toolkit_user_valves_spec_by_id( if id in request.app.state.TOOLS: toolkit_module = request.app.state.TOOLS[id] else: - toolkit_module = load_toolkit_module_by_id(id) + toolkit_module, frontmatter = load_toolkit_module_by_id(id) request.app.state.TOOLS[id] = toolkit_module if hasattr(toolkit_module, "UserValves"): @@ -276,7 +344,7 @@ async def update_toolkit_user_valves_by_id( if id in request.app.state.TOOLS: toolkit_module = request.app.state.TOOLS[id] else: - toolkit_module = load_toolkit_module_by_id(id) + toolkit_module, frontmatter = load_toolkit_module_by_id(id) request.app.state.TOOLS[id] = toolkit_module if hasattr(toolkit_module, "UserValves"): @@ -305,69 +373,3 @@ async def update_toolkit_user_valves_by_id( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND, ) - - -############################ -# UpdateToolkitById -############################ - - -@router.post("/id/{id}/update", response_model=Optional[ToolModel]) -async def update_toolkit_by_id( - request: Request, id: str, form_data: ToolForm, user=Depends(get_admin_user) -): - toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py") - - try: - with open(toolkit_path, "w") as tool_file: - tool_file.write(form_data.content) - - toolkit_module = load_toolkit_module_by_id(id) - - TOOLS = request.app.state.TOOLS - TOOLS[id] = toolkit_module - - specs = get_tools_specs(TOOLS[id]) - - updated = { - **form_data.model_dump(exclude={"id"}), - "specs": specs, - } - - print(updated) - toolkit = Tools.update_tool_by_id(id, updated) - - if toolkit: - return toolkit - else: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error updating toolkit"), - ) - - except Exception as e: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT(e), - ) - - -############################ -# DeleteToolkitById -############################ - - -@router.delete("/id/{id}/delete", response_model=bool) -async def delete_toolkit_by_id(request: Request, id: str, user=Depends(get_admin_user)): - result = Tools.delete_tool_by_id(id) - - if result: - TOOLS = request.app.state.TOOLS - if id in TOOLS: - del TOOLS[id] - - # delete the toolkit file - toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py") - os.remove(toolkit_path) - - return result diff --git a/backend/apps/webui/utils.py b/backend/apps/webui/utils.py index 3e075a8a8..f50b13f38 100644 --- a/backend/apps/webui/utils.py +++ b/backend/apps/webui/utils.py @@ -1,19 +1,41 @@ from importlib import util import os +import re from config import TOOLS_DIR, FUNCTIONS_DIR +def extract_frontmatter(file_path): + """ + Extract frontmatter as a dictionary from the specified file path. + """ + frontmatter = {} + frontmatter_pattern = re.compile(r"^([a-z_]+):\s*(.*)\s*$", re.IGNORECASE) + + with open(file_path, "r", encoding="utf-8") as file: + for line in file: + if line.strip() == '"""': + # End of frontmatter section + break + match = frontmatter_pattern.match(line) + if match: + key, value = match.groups() + frontmatter[key] = value + + return frontmatter + + def load_toolkit_module_by_id(toolkit_id): toolkit_path = os.path.join(TOOLS_DIR, f"{toolkit_id}.py") spec = util.spec_from_file_location(toolkit_id, toolkit_path) module = util.module_from_spec(spec) + frontmatter = extract_frontmatter(toolkit_path) try: spec.loader.exec_module(module) print(f"Loaded module: {module.__name__}") if hasattr(module, "Tools"): - return module.Tools() + return module.Tools(), frontmatter else: raise Exception("No Tools class found") except Exception as e: @@ -28,14 +50,15 @@ def load_function_module_by_id(function_id): spec = util.spec_from_file_location(function_id, function_path) module = util.module_from_spec(spec) + frontmatter = extract_frontmatter(function_path) try: spec.loader.exec_module(module) print(f"Loaded module: {module.__name__}") if hasattr(module, "Pipe"): - return module.Pipe(), "pipe" + return module.Pipe(), "pipe", frontmatter elif hasattr(module, "Filter"): - return module.Filter(), "filter" + return module.Filter(), "filter", frontmatter else: raise Exception("No Function class found") except Exception as e: diff --git a/backend/main.py b/backend/main.py index 85fe16d3b..12e2d937e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -258,7 +258,7 @@ async def get_function_call_response( if tool_id in webui_app.state.TOOLS: toolkit_module = webui_app.state.TOOLS[tool_id] else: - toolkit_module = load_toolkit_module_by_id(tool_id) + toolkit_module, frontmatter = load_toolkit_module_by_id(tool_id) webui_app.state.TOOLS[tool_id] = toolkit_module file_handler = False @@ -415,8 +415,8 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware): if filter_id in webui_app.state.FUNCTIONS: function_module = webui_app.state.FUNCTIONS[filter_id] else: - function_module, function_type = load_function_module_by_id( - filter_id + function_module, function_type, frontmatter = ( + load_function_module_by_id(filter_id) ) webui_app.state.FUNCTIONS[filter_id] = function_module @@ -909,7 +909,9 @@ async def generate_chat_completions(form_data: dict, user=Depends(get_verified_u # Check if function is already loaded if pipe_id not in webui_app.state.FUNCTIONS: - function_module, function_type = load_function_module_by_id(pipe_id) + function_module, function_type, frontmatter = ( + load_function_module_by_id(pipe_id) + ) webui_app.state.FUNCTIONS[pipe_id] = function_module else: function_module = webui_app.state.FUNCTIONS[pipe_id] @@ -1155,7 +1157,9 @@ async def chat_completed(form_data: dict, user=Depends(get_verified_user)): if filter_id in webui_app.state.FUNCTIONS: function_module = webui_app.state.FUNCTIONS[filter_id] else: - function_module, function_type = load_function_module_by_id(filter_id) + function_module, function_type, frontmatter = ( + load_function_module_by_id(filter_id) + ) webui_app.state.FUNCTIONS[filter_id] = function_module if hasattr(function_module, "valves") and hasattr(