mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-17 14:55:23 +00:00
commit
e6fe3ada57
23 changed files with 1391 additions and 99 deletions
61
backend/apps/webui/internal/migrations/012_add_tools.py
Normal file
61
backend/apps/webui/internal/migrations/012_add_tools.py
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
"""Peewee migrations -- 009_add_models.py.
|
||||||
|
|
||||||
|
Some examples (model - class or model name)::
|
||||||
|
|
||||||
|
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||||
|
> Model = migrator.ModelClass # Return model in current state by name
|
||||||
|
|
||||||
|
> migrator.sql(sql) # Run custom SQL
|
||||||
|
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||||
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
> migrator.change_fields(model, **fields) # Change fields
|
||||||
|
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||||
|
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||||
|
> migrator.rename_table(model, new_table_name)
|
||||||
|
> migrator.add_index(model, *col_names, unique=False)
|
||||||
|
> migrator.add_not_null(model, *field_names)
|
||||||
|
> migrator.add_default(model, field_name, default)
|
||||||
|
> migrator.add_constraint(model, name, sql)
|
||||||
|
> migrator.drop_index(model, *col_names)
|
||||||
|
> migrator.drop_not_null(model, *field_names)
|
||||||
|
> migrator.drop_constraints(model, *constraints)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
import peewee as pw
|
||||||
|
from peewee_migrate import Migrator
|
||||||
|
|
||||||
|
|
||||||
|
with suppress(ImportError):
|
||||||
|
import playhouse.postgres_ext as pw_pext
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your migrations here."""
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class Tool(pw.Model):
|
||||||
|
id = pw.TextField(unique=True)
|
||||||
|
user_id = pw.TextField()
|
||||||
|
|
||||||
|
name = pw.TextField()
|
||||||
|
content = pw.TextField()
|
||||||
|
specs = pw.TextField()
|
||||||
|
|
||||||
|
meta = pw.TextField()
|
||||||
|
|
||||||
|
created_at = pw.BigIntegerField(null=False)
|
||||||
|
updated_at = pw.BigIntegerField(null=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "tool"
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your rollback migrations here."""
|
||||||
|
|
||||||
|
migrator.remove_model("tool")
|
||||||
|
|
@ -6,6 +6,7 @@ from apps.webui.routers import (
|
||||||
users,
|
users,
|
||||||
chats,
|
chats,
|
||||||
documents,
|
documents,
|
||||||
|
tools,
|
||||||
models,
|
models,
|
||||||
prompts,
|
prompts,
|
||||||
configs,
|
configs,
|
||||||
|
|
@ -26,8 +27,8 @@ from config import (
|
||||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
|
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
|
||||||
JWT_EXPIRES_IN,
|
JWT_EXPIRES_IN,
|
||||||
WEBUI_BANNERS,
|
WEBUI_BANNERS,
|
||||||
AppConfig,
|
|
||||||
ENABLE_COMMUNITY_SHARING,
|
ENABLE_COMMUNITY_SHARING,
|
||||||
|
AppConfig,
|
||||||
)
|
)
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
@ -38,6 +39,7 @@ app.state.config = AppConfig()
|
||||||
|
|
||||||
app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
|
app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
|
||||||
app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
|
app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
|
||||||
|
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
||||||
|
|
||||||
|
|
||||||
app.state.config.SHOW_ADMIN_DETAILS = SHOW_ADMIN_DETAILS
|
app.state.config.SHOW_ADMIN_DETAILS = SHOW_ADMIN_DETAILS
|
||||||
|
|
@ -54,7 +56,7 @@ app.state.config.BANNERS = WEBUI_BANNERS
|
||||||
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
|
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
|
||||||
|
|
||||||
app.state.MODELS = {}
|
app.state.MODELS = {}
|
||||||
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
app.state.TOOLS = {}
|
||||||
|
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
|
|
@ -70,6 +72,7 @@ app.include_router(users.router, prefix="/users", tags=["users"])
|
||||||
app.include_router(chats.router, prefix="/chats", tags=["chats"])
|
app.include_router(chats.router, prefix="/chats", tags=["chats"])
|
||||||
|
|
||||||
app.include_router(documents.router, prefix="/documents", tags=["documents"])
|
app.include_router(documents.router, prefix="/documents", tags=["documents"])
|
||||||
|
app.include_router(tools.router, prefix="/tools", tags=["tools"])
|
||||||
app.include_router(models.router, prefix="/models", tags=["models"])
|
app.include_router(models.router, prefix="/models", tags=["models"])
|
||||||
app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
|
app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
|
||||||
app.include_router(memories.router, prefix="/memories", tags=["memories"])
|
app.include_router(memories.router, prefix="/memories", tags=["memories"])
|
||||||
|
|
|
||||||
131
backend/apps/webui/models/tools.py
Normal file
131
backend/apps/webui/models/tools.py
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from peewee import *
|
||||||
|
from playhouse.shortcuts import model_to_dict
|
||||||
|
from typing import List, Union, Optional
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from apps.webui.internal.db import DB, JSONField
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from config import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
||||||
|
####################
|
||||||
|
# Tools DB Schema
|
||||||
|
####################
|
||||||
|
|
||||||
|
|
||||||
|
class Tool(Model):
|
||||||
|
id = CharField(unique=True)
|
||||||
|
user_id = CharField()
|
||||||
|
name = TextField()
|
||||||
|
content = TextField()
|
||||||
|
specs = JSONField()
|
||||||
|
meta = JSONField()
|
||||||
|
updated_at = BigIntegerField()
|
||||||
|
created_at = BigIntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
database = DB
|
||||||
|
|
||||||
|
|
||||||
|
class ToolMeta(BaseModel):
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ToolModel(BaseModel):
|
||||||
|
id: str
|
||||||
|
user_id: str
|
||||||
|
name: str
|
||||||
|
content: str
|
||||||
|
specs: List[dict]
|
||||||
|
meta: ToolMeta
|
||||||
|
updated_at: int # timestamp in epoch
|
||||||
|
created_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# Forms
|
||||||
|
####################
|
||||||
|
|
||||||
|
|
||||||
|
class ToolResponse(BaseModel):
|
||||||
|
id: str
|
||||||
|
user_id: str
|
||||||
|
name: str
|
||||||
|
meta: ToolMeta
|
||||||
|
updated_at: int # timestamp in epoch
|
||||||
|
created_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
|
||||||
|
class ToolForm(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
content: str
|
||||||
|
meta: ToolMeta
|
||||||
|
|
||||||
|
|
||||||
|
class ToolsTable:
|
||||||
|
def __init__(self, db):
|
||||||
|
self.db = db
|
||||||
|
self.db.create_tables([Tool])
|
||||||
|
|
||||||
|
def insert_new_tool(
|
||||||
|
self, user_id: str, form_data: ToolForm, specs: List[dict]
|
||||||
|
) -> Optional[ToolModel]:
|
||||||
|
tool = ToolModel(
|
||||||
|
**{
|
||||||
|
**form_data.model_dump(),
|
||||||
|
"specs": specs,
|
||||||
|
"user_id": user_id,
|
||||||
|
"updated_at": int(time.time()),
|
||||||
|
"created_at": int(time.time()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = Tool.create(**tool.model_dump())
|
||||||
|
if result:
|
||||||
|
return tool
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_tool_by_id(self, id: str) -> Optional[ToolModel]:
|
||||||
|
try:
|
||||||
|
tool = Tool.get(Tool.id == id)
|
||||||
|
return ToolModel(**model_to_dict(tool))
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_tools(self) -> List[ToolModel]:
|
||||||
|
return [ToolModel(**model_to_dict(tool)) for tool in Tool.select()]
|
||||||
|
|
||||||
|
def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]:
|
||||||
|
try:
|
||||||
|
query = Tool.update(
|
||||||
|
**updated,
|
||||||
|
updated_at=int(time.time()),
|
||||||
|
).where(Tool.id == id)
|
||||||
|
query.execute()
|
||||||
|
|
||||||
|
tool = Tool.get(Tool.id == id)
|
||||||
|
return ToolModel(**model_to_dict(tool))
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def delete_tool_by_id(self, id: str) -> bool:
|
||||||
|
try:
|
||||||
|
query = Tool.delete().where((Tool.id == id))
|
||||||
|
query.execute() # Remove the rows, return number of rows removed.
|
||||||
|
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
Tools = ToolsTable(DB)
|
||||||
177
backend/apps/webui/routers/tools.py
Normal file
177
backend/apps/webui/routers/tools.py
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
from fastapi import Depends, FastAPI, HTTPException, status, Request
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import List, Union, Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from pydantic import BaseModel
|
||||||
|
import json
|
||||||
|
|
||||||
|
from apps.webui.models.tools import Tools, ToolForm, ToolModel, ToolResponse
|
||||||
|
from apps.webui.utils import load_toolkit_module_by_id
|
||||||
|
|
||||||
|
from utils.utils import get_current_user, get_admin_user
|
||||||
|
from utils.tools import get_tools_specs
|
||||||
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
|
from importlib import util
|
||||||
|
import os
|
||||||
|
|
||||||
|
from config import DATA_DIR
|
||||||
|
|
||||||
|
|
||||||
|
TOOLS_DIR = f"{DATA_DIR}/tools"
|
||||||
|
os.makedirs(TOOLS_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
############################
|
||||||
|
# GetToolkits
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[ToolResponse])
|
||||||
|
async def get_toolkits(user=Depends(get_current_user)):
|
||||||
|
toolkits = [toolkit for toolkit in Tools.get_tools()]
|
||||||
|
return toolkits
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# ExportToolKits
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/export", response_model=List[ToolModel])
|
||||||
|
async def get_toolkits(user=Depends(get_admin_user)):
|
||||||
|
toolkits = [toolkit for toolkit in Tools.get_tools()]
|
||||||
|
return toolkits
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# CreateNewToolKit
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/create", response_model=Optional[ToolResponse])
|
||||||
|
async def create_new_toolkit(
|
||||||
|
request: Request, form_data: ToolForm, user=Depends(get_admin_user)
|
||||||
|
):
|
||||||
|
if not form_data.id.isidentifier():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Only alphanumeric characters and underscores are allowed in the id",
|
||||||
|
)
|
||||||
|
|
||||||
|
form_data.id = form_data.id.lower()
|
||||||
|
|
||||||
|
toolkit = Tools.get_tool_by_id(form_data.id)
|
||||||
|
if toolkit == None:
|
||||||
|
toolkit_path = os.path.join(TOOLS_DIR, f"{form_data.id}.py")
|
||||||
|
try:
|
||||||
|
with open(toolkit_path, "w") as tool_file:
|
||||||
|
tool_file.write(form_data.content)
|
||||||
|
|
||||||
|
toolkit_module = load_toolkit_module_by_id(form_data.id)
|
||||||
|
|
||||||
|
TOOLS = request.app.state.TOOLS
|
||||||
|
TOOLS[form_data.id] = toolkit_module
|
||||||
|
|
||||||
|
specs = get_tools_specs(TOOLS[form_data.id])
|
||||||
|
toolkit = Tools.insert_new_tool(user.id, form_data, specs)
|
||||||
|
|
||||||
|
if toolkit:
|
||||||
|
return toolkit
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.FILE_EXISTS,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.ID_TAKEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# GetToolkitById
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/id/{id}", response_model=Optional[ToolModel])
|
||||||
|
async def get_toolkit_by_id(id: str, user=Depends(get_admin_user)):
|
||||||
|
toolkit = Tools.get_tool_by_id(id)
|
||||||
|
|
||||||
|
if toolkit:
|
||||||
|
return toolkit
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
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
|
||||||
|
del TOOLS[id]
|
||||||
|
|
||||||
|
return result
|
||||||
23
backend/apps/webui/utils.py
Normal file
23
backend/apps/webui/utils.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
from importlib import util
|
||||||
|
import os
|
||||||
|
|
||||||
|
from config import TOOLS_DIR
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
try:
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
print(f"Loaded module: {module.__name__}")
|
||||||
|
if hasattr(module, "Tools"):
|
||||||
|
return module.Tools()
|
||||||
|
else:
|
||||||
|
raise Exception("No Tools class found")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading module: {toolkit_id}")
|
||||||
|
# Move the file to the error folder
|
||||||
|
os.rename(toolkit_path, f"{toolkit_path}.error")
|
||||||
|
raise e
|
||||||
|
|
@ -368,6 +368,14 @@ DOCS_DIR = os.getenv("DOCS_DIR", f"{DATA_DIR}/docs")
|
||||||
Path(DOCS_DIR).mkdir(parents=True, exist_ok=True)
|
Path(DOCS_DIR).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# Tools DIR
|
||||||
|
####################################
|
||||||
|
|
||||||
|
TOOLS_DIR = os.getenv("TOOLS_DIR", f"{DATA_DIR}/tools")
|
||||||
|
Path(TOOLS_DIR).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# LITELLM_CONFIG
|
# LITELLM_CONFIG
|
||||||
####################################
|
####################################
|
||||||
|
|
@ -669,7 +677,6 @@ Question:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = PersistentConfig(
|
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = PersistentConfig(
|
||||||
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD",
|
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD",
|
||||||
"task.search.prompt_length_threshold",
|
"task.search.prompt_length_threshold",
|
||||||
|
|
@ -679,6 +686,17 @@ SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = PersistentConfig(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig(
|
||||||
|
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
|
||||||
|
"task.tools.prompt_template",
|
||||||
|
os.environ.get(
|
||||||
|
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
|
||||||
|
"""Tools: {{TOOLS}}
|
||||||
|
If a function tool doesn't match the query, return an empty string. Else, pick a function tool, fill in the parameters from the function tool's schema, and return it in the format { "name": \"functionName\", "parameters": { "key": "value" } }. Only pick a function if the user asks. Only return the object. Do not return any other text.""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# WEBUI_SECRET_KEY
|
# WEBUI_SECRET_KEY
|
||||||
####################################
|
####################################
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ class ERROR_MESSAGES(str, Enum):
|
||||||
COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string."
|
COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string."
|
||||||
FILE_EXISTS = "Uh-oh! This file is already registered. Please choose another file."
|
FILE_EXISTS = "Uh-oh! This file is already registered. Please choose another file."
|
||||||
|
|
||||||
|
ID_TAKEN = "Uh-oh! This id is already registered. Please choose another id string."
|
||||||
MODEL_ID_TAKEN = "Uh-oh! This model id is already registered. Please choose another model id string."
|
MODEL_ID_TAKEN = "Uh-oh! This model id is already registered. Please choose another model id string."
|
||||||
|
|
||||||
NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string."
|
NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string."
|
||||||
|
|
|
||||||
193
backend/main.py
193
backend/main.py
|
|
@ -47,15 +47,24 @@ from pydantic import BaseModel
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from apps.webui.models.models import Models, ModelModel
|
from apps.webui.models.models import Models, ModelModel
|
||||||
|
from apps.webui.models.tools import Tools
|
||||||
|
from apps.webui.utils import load_toolkit_module_by_id
|
||||||
|
|
||||||
|
|
||||||
from utils.utils import (
|
from utils.utils import (
|
||||||
get_admin_user,
|
get_admin_user,
|
||||||
get_verified_user,
|
get_verified_user,
|
||||||
get_current_user,
|
get_current_user,
|
||||||
get_http_authorization_cred,
|
get_http_authorization_cred,
|
||||||
)
|
)
|
||||||
from utils.task import title_generation_template, search_query_generation_template
|
from utils.task import (
|
||||||
|
title_generation_template,
|
||||||
|
search_query_generation_template,
|
||||||
|
tools_function_calling_generation_template,
|
||||||
|
)
|
||||||
|
from utils.misc import get_last_user_message, add_or_update_system_message
|
||||||
|
|
||||||
from apps.rag.utils import rag_messages
|
from apps.rag.utils import rag_messages, rag_template
|
||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
CONFIG_DATA,
|
CONFIG_DATA,
|
||||||
|
|
@ -82,6 +91,7 @@ from config import (
|
||||||
TITLE_GENERATION_PROMPT_TEMPLATE,
|
TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||||
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
|
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
|
||||||
|
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
|
||||||
AppConfig,
|
AppConfig,
|
||||||
)
|
)
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
@ -148,24 +158,80 @@ app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
|
||||||
app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = (
|
app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = (
|
||||||
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD
|
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD
|
||||||
)
|
)
|
||||||
|
app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
|
||||||
|
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
|
||||||
|
)
|
||||||
|
|
||||||
app.state.MODELS = {}
|
app.state.MODELS = {}
|
||||||
|
|
||||||
origins = ["*"]
|
origins = ["*"]
|
||||||
|
|
||||||
# Custom middleware to add security headers
|
|
||||||
# class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
async def get_function_call_response(prompt, tool_id, template, task_model_id, user):
|
||||||
# async def dispatch(self, request: Request, call_next):
|
tool = Tools.get_tool_by_id(tool_id)
|
||||||
# response: Response = await call_next(request)
|
tools_specs = json.dumps(tool.specs, indent=2)
|
||||||
# response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
|
content = tools_function_calling_generation_template(template, tools_specs)
|
||||||
# response.headers["Cross-Origin-Embedder-Policy"] = "require-corp"
|
|
||||||
# return response
|
payload = {
|
||||||
|
"model": task_model_id,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": content},
|
||||||
|
{"role": "user", "content": f"Query: {prompt}"},
|
||||||
|
],
|
||||||
|
"stream": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = filter_pipeline(payload, user)
|
||||||
|
model = app.state.MODELS[task_model_id]
|
||||||
|
|
||||||
|
response = None
|
||||||
|
try:
|
||||||
|
if model["owned_by"] == "ollama":
|
||||||
|
response = await generate_ollama_chat_completion(
|
||||||
|
OpenAIChatCompletionForm(**payload), user=user
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response = await generate_openai_chat_completion(payload, user=user)
|
||||||
|
|
||||||
|
content = None
|
||||||
|
async for chunk in response.body_iterator:
|
||||||
|
data = json.loads(chunk.decode("utf-8"))
|
||||||
|
content = data["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
|
# Cleanup any remaining background tasks if necessary
|
||||||
|
if response.background is not None:
|
||||||
|
await response.background()
|
||||||
|
|
||||||
|
# Parse the function response
|
||||||
|
if content is not None:
|
||||||
|
result = json.loads(content)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
# Call the function
|
||||||
|
if "name" in result:
|
||||||
|
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)
|
||||||
|
webui_app.state.TOOLS[tool_id] = toolkit_module
|
||||||
|
|
||||||
|
function = getattr(toolkit_module, result["name"])
|
||||||
|
function_result = None
|
||||||
|
try:
|
||||||
|
function_result = function(**result["parameters"])
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
# Add the function result to the system prompt
|
||||||
|
if function_result:
|
||||||
|
return function_result
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# app.add_middleware(SecurityHeadersMiddleware)
|
class ChatCompletionMiddleware(BaseHTTPMiddleware):
|
||||||
|
|
||||||
|
|
||||||
class RAGMiddleware(BaseHTTPMiddleware):
|
|
||||||
async def dispatch(self, request: Request, call_next):
|
async def dispatch(self, request: Request, call_next):
|
||||||
return_citations = False
|
return_citations = False
|
||||||
|
|
||||||
|
|
@ -182,12 +248,68 @@ class RAGMiddleware(BaseHTTPMiddleware):
|
||||||
# Parse string to JSON
|
# Parse string to JSON
|
||||||
data = json.loads(body_str) if body_str else {}
|
data = json.loads(body_str) if body_str else {}
|
||||||
|
|
||||||
|
# Remove the citations from the body
|
||||||
return_citations = data.get("citations", False)
|
return_citations = data.get("citations", False)
|
||||||
if "citations" in data:
|
if "citations" in data:
|
||||||
del data["citations"]
|
del data["citations"]
|
||||||
|
|
||||||
# Example: Add a new key-value pair or modify existing ones
|
# Set the task model
|
||||||
# data["modified"] = True # Example modification
|
task_model_id = data["model"]
|
||||||
|
if task_model_id not in app.state.MODELS:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Model not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the user has a custom task model
|
||||||
|
# If the user has a custom task model, use that model
|
||||||
|
if app.state.MODELS[task_model_id]["owned_by"] == "ollama":
|
||||||
|
if (
|
||||||
|
app.state.config.TASK_MODEL
|
||||||
|
and app.state.config.TASK_MODEL in app.state.MODELS
|
||||||
|
):
|
||||||
|
task_model_id = app.state.config.TASK_MODEL
|
||||||
|
else:
|
||||||
|
if (
|
||||||
|
app.state.config.TASK_MODEL_EXTERNAL
|
||||||
|
and app.state.config.TASK_MODEL_EXTERNAL in app.state.MODELS
|
||||||
|
):
|
||||||
|
task_model_id = app.state.config.TASK_MODEL_EXTERNAL
|
||||||
|
|
||||||
|
if "tool_ids" in data:
|
||||||
|
user = get_current_user(
|
||||||
|
get_http_authorization_cred(request.headers.get("Authorization"))
|
||||||
|
)
|
||||||
|
prompt = get_last_user_message(data["messages"])
|
||||||
|
context = ""
|
||||||
|
|
||||||
|
for tool_id in data["tool_ids"]:
|
||||||
|
print(tool_id)
|
||||||
|
response = await get_function_call_response(
|
||||||
|
prompt=prompt,
|
||||||
|
tool_id=tool_id,
|
||||||
|
template=app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
|
||||||
|
task_model_id=task_model_id,
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
|
||||||
|
if response:
|
||||||
|
context += ("\n" if context != "" else "") + response
|
||||||
|
|
||||||
|
if context != "":
|
||||||
|
system_prompt = rag_template(
|
||||||
|
rag_app.state.config.RAG_TEMPLATE, context, prompt
|
||||||
|
)
|
||||||
|
|
||||||
|
print(system_prompt)
|
||||||
|
|
||||||
|
data["messages"] = add_or_update_system_message(
|
||||||
|
f"\n{system_prompt}", data["messages"]
|
||||||
|
)
|
||||||
|
|
||||||
|
del data["tool_ids"]
|
||||||
|
|
||||||
|
# If docs field is present, generate RAG completions
|
||||||
if "docs" in data:
|
if "docs" in data:
|
||||||
data = {**data}
|
data = {**data}
|
||||||
data["messages"], citations = rag_messages(
|
data["messages"], citations = rag_messages(
|
||||||
|
|
@ -210,7 +332,6 @@ class RAGMiddleware(BaseHTTPMiddleware):
|
||||||
|
|
||||||
# Replace the request body with the modified one
|
# Replace the request body with the modified one
|
||||||
request._body = modified_body_bytes
|
request._body = modified_body_bytes
|
||||||
|
|
||||||
# Set custom header to ensure content-length matches new body length
|
# Set custom header to ensure content-length matches new body length
|
||||||
request.headers.__dict__["_list"] = [
|
request.headers.__dict__["_list"] = [
|
||||||
(b"content-length", str(len(modified_body_bytes)).encode("utf-8")),
|
(b"content-length", str(len(modified_body_bytes)).encode("utf-8")),
|
||||||
|
|
@ -253,7 +374,7 @@ class RAGMiddleware(BaseHTTPMiddleware):
|
||||||
yield data
|
yield data
|
||||||
|
|
||||||
|
|
||||||
app.add_middleware(RAGMiddleware)
|
app.add_middleware(ChatCompletionMiddleware)
|
||||||
|
|
||||||
|
|
||||||
def filter_pipeline(payload, user):
|
def filter_pipeline(payload, user):
|
||||||
|
|
@ -515,6 +636,7 @@ async def get_task_config(user=Depends(get_verified_user)):
|
||||||
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||||
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||||
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD": app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
|
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD": app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
|
||||||
|
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -524,6 +646,7 @@ class TaskConfigForm(BaseModel):
|
||||||
TITLE_GENERATION_PROMPT_TEMPLATE: str
|
TITLE_GENERATION_PROMPT_TEMPLATE: str
|
||||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: str
|
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: str
|
||||||
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD: int
|
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD: int
|
||||||
|
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE: str
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/task/config/update")
|
@app.post("/api/task/config/update")
|
||||||
|
|
@ -539,6 +662,9 @@ async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_u
|
||||||
app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = (
|
app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = (
|
||||||
form_data.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD
|
form_data.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD
|
||||||
)
|
)
|
||||||
|
app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
|
||||||
|
form_data.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"TASK_MODEL": app.state.config.TASK_MODEL,
|
"TASK_MODEL": app.state.config.TASK_MODEL,
|
||||||
|
|
@ -546,6 +672,7 @@ async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_u
|
||||||
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||||
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||||
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD": app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
|
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD": app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
|
||||||
|
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -659,6 +786,38 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
|
||||||
return await generate_openai_chat_completion(payload, user=user)
|
return await generate_openai_chat_completion(payload, user=user)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/task/tools/completions")
|
||||||
|
async def get_tools_function_calling(form_data: dict, user=Depends(get_verified_user)):
|
||||||
|
print("get_tools_function_calling")
|
||||||
|
|
||||||
|
model_id = form_data["model"]
|
||||||
|
if model_id not in app.state.MODELS:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Model not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the user has a custom task model
|
||||||
|
# If the user has a custom task model, use that model
|
||||||
|
if app.state.MODELS[model_id]["owned_by"] == "ollama":
|
||||||
|
if app.state.config.TASK_MODEL:
|
||||||
|
task_model_id = app.state.config.TASK_MODEL
|
||||||
|
if task_model_id in app.state.MODELS:
|
||||||
|
model_id = task_model_id
|
||||||
|
else:
|
||||||
|
if app.state.config.TASK_MODEL_EXTERNAL:
|
||||||
|
task_model_id = app.state.config.TASK_MODEL_EXTERNAL
|
||||||
|
if task_model_id in app.state.MODELS:
|
||||||
|
model_id = task_model_id
|
||||||
|
|
||||||
|
print(model_id)
|
||||||
|
template = app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
|
||||||
|
|
||||||
|
return await get_function_call_response(
|
||||||
|
form_data["prompt"], form_data["tool_id"], template, model_id, user
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/chat/completions")
|
@app.post("/api/chat/completions")
|
||||||
async def generate_chat_completions(form_data: dict, user=Depends(get_verified_user)):
|
async def generate_chat_completions(form_data: dict, user=Depends(get_verified_user)):
|
||||||
model_id = form_data["model"]
|
model_id = form_data["model"]
|
||||||
|
|
|
||||||
|
|
@ -110,3 +110,8 @@ def search_query_generation_template(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return template
|
return template
|
||||||
|
|
||||||
|
|
||||||
|
def tools_function_calling_generation_template(template: str, tools_specs: str) -> str:
|
||||||
|
template = template.replace("{{TOOLS}}", tools_specs)
|
||||||
|
return template
|
||||||
|
|
|
||||||
73
backend/utils/tools.py
Normal file
73
backend/utils/tools.py
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import inspect
|
||||||
|
from typing import get_type_hints, List, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
def doc_to_dict(docstring):
|
||||||
|
lines = docstring.split("\n")
|
||||||
|
description = lines[1].strip()
|
||||||
|
param_dict = {}
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if ":param" in line:
|
||||||
|
line = line.replace(":param", "").strip()
|
||||||
|
param, desc = line.split(":", 1)
|
||||||
|
param_dict[param.strip()] = desc.strip()
|
||||||
|
ret_dict = {"description": description, "params": param_dict}
|
||||||
|
return ret_dict
|
||||||
|
|
||||||
|
|
||||||
|
def get_tools_specs(tools) -> List[dict]:
|
||||||
|
function_list = [
|
||||||
|
{"name": func, "function": getattr(tools, func)}
|
||||||
|
for func in dir(tools)
|
||||||
|
if callable(getattr(tools, func)) and not func.startswith("__")
|
||||||
|
]
|
||||||
|
|
||||||
|
specs = []
|
||||||
|
for function_item in function_list:
|
||||||
|
function_name = function_item["name"]
|
||||||
|
function = function_item["function"]
|
||||||
|
|
||||||
|
function_doc = doc_to_dict(function.__doc__ or function_name)
|
||||||
|
specs.append(
|
||||||
|
{
|
||||||
|
"name": function_name,
|
||||||
|
# TODO: multi-line desc?
|
||||||
|
"description": function_doc.get("description", function_name),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
param_name: {
|
||||||
|
"type": param_annotation.__name__.lower(),
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"enum": (
|
||||||
|
param_annotation.__args__
|
||||||
|
if hasattr(param_annotation, "__args__")
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if hasattr(param_annotation, "__args__")
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
"description": function_doc.get("params", {}).get(
|
||||||
|
param_name, param_name
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for param_name, param_annotation in get_type_hints(
|
||||||
|
function
|
||||||
|
).items()
|
||||||
|
if param_name != "return"
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
name
|
||||||
|
for name, param in inspect.signature(
|
||||||
|
function
|
||||||
|
).parameters.items()
|
||||||
|
if param.default is param.empty
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return specs
|
||||||
193
src/lib/apis/tools/index.ts
Normal file
193
src/lib/apis/tools/index.ts
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
|
export const createNewTool = async (token: string, tool: object) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/tools/create`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...tool
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTools = async (token: string = '') => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/tools`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exportTools = async (token: string = '') => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/tools/export`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getToolById = async (token: string, id: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateToolById = async (token: string, id: string, tool: object) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/update`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...tool
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteToolById = async (token: string, id: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/delete`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
@ -24,7 +24,8 @@
|
||||||
banners,
|
banners,
|
||||||
user,
|
user,
|
||||||
socket,
|
socket,
|
||||||
showCallOverlay
|
showCallOverlay,
|
||||||
|
tools
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import {
|
import {
|
||||||
convertMessagesToHistory,
|
convertMessagesToHistory,
|
||||||
|
|
@ -73,6 +74,7 @@
|
||||||
let selectedModels = [''];
|
let selectedModels = [''];
|
||||||
let atSelectedModel: Model | undefined;
|
let atSelectedModel: Model | undefined;
|
||||||
|
|
||||||
|
let selectedToolIds = [];
|
||||||
let webSearchEnabled = false;
|
let webSearchEnabled = false;
|
||||||
|
|
||||||
let chat = null;
|
let chat = null;
|
||||||
|
|
@ -687,6 +689,7 @@
|
||||||
},
|
},
|
||||||
format: $settings.requestFormat ?? undefined,
|
format: $settings.requestFormat ?? undefined,
|
||||||
keep_alive: $settings.keepAlive ?? undefined,
|
keep_alive: $settings.keepAlive ?? undefined,
|
||||||
|
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
|
||||||
docs: docs.length > 0 ? docs : undefined,
|
docs: docs.length > 0 ? docs : undefined,
|
||||||
citations: docs.length > 0,
|
citations: docs.length > 0,
|
||||||
chat_id: $chatId
|
chat_id: $chatId
|
||||||
|
|
@ -948,6 +951,7 @@
|
||||||
top_p: $settings?.params?.top_p ?? undefined,
|
top_p: $settings?.params?.top_p ?? undefined,
|
||||||
frequency_penalty: $settings?.params?.frequency_penalty ?? undefined,
|
frequency_penalty: $settings?.params?.frequency_penalty ?? undefined,
|
||||||
max_tokens: $settings?.params?.max_tokens ?? undefined,
|
max_tokens: $settings?.params?.max_tokens ?? undefined,
|
||||||
|
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
|
||||||
docs: docs.length > 0 ? docs : undefined,
|
docs: docs.length > 0 ? docs : undefined,
|
||||||
citations: docs.length > 0,
|
citations: docs.length > 0,
|
||||||
chat_id: $chatId
|
chat_id: $chatId
|
||||||
|
|
@ -1274,8 +1278,20 @@
|
||||||
bind:files
|
bind:files
|
||||||
bind:prompt
|
bind:prompt
|
||||||
bind:autoScroll
|
bind:autoScroll
|
||||||
|
bind:selectedToolIds
|
||||||
bind:webSearchEnabled
|
bind:webSearchEnabled
|
||||||
bind:atSelectedModel
|
bind:atSelectedModel
|
||||||
|
availableTools={$user.role === 'admin'
|
||||||
|
? $tools.reduce((a, e, i, arr) => {
|
||||||
|
a[e.id] = {
|
||||||
|
name: e.name,
|
||||||
|
description: e.meta.description,
|
||||||
|
enabled: false
|
||||||
|
};
|
||||||
|
|
||||||
|
return a;
|
||||||
|
}, {})
|
||||||
|
: {}}
|
||||||
{selectedModels}
|
{selectedModels}
|
||||||
{messages}
|
{messages}
|
||||||
{submitPrompt}
|
{submitPrompt}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
showSidebar,
|
showSidebar,
|
||||||
models,
|
models,
|
||||||
config,
|
config,
|
||||||
showCallOverlay
|
showCallOverlay,
|
||||||
|
tools
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
|
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
|
||||||
|
|
||||||
|
|
@ -58,6 +59,8 @@
|
||||||
|
|
||||||
export let files = [];
|
export let files = [];
|
||||||
|
|
||||||
|
export let availableTools = {};
|
||||||
|
export let selectedToolIds = [];
|
||||||
export let webSearchEnabled = false;
|
export let webSearchEnabled = false;
|
||||||
|
|
||||||
export let prompt = '';
|
export let prompt = '';
|
||||||
|
|
@ -653,6 +656,8 @@
|
||||||
<div class=" ml-0.5 self-end mb-1.5 flex space-x-1">
|
<div class=" ml-0.5 self-end mb-1.5 flex space-x-1">
|
||||||
<InputMenu
|
<InputMenu
|
||||||
bind:webSearchEnabled
|
bind:webSearchEnabled
|
||||||
|
bind:selectedToolIds
|
||||||
|
tools={availableTools}
|
||||||
uploadFilesHandler={() => {
|
uploadFilesHandler={() => {
|
||||||
filesInputElement.click();
|
filesInputElement.click();
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -4,22 +4,21 @@
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
|
|
||||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||||
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
|
|
||||||
import Pencil from '$lib/components/icons/Pencil.svelte';
|
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import Tags from '$lib/components/chat/Tags.svelte';
|
|
||||||
import Share from '$lib/components/icons/Share.svelte';
|
|
||||||
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
|
||||||
import DocumentArrowUpSolid from '$lib/components/icons/DocumentArrowUpSolid.svelte';
|
import DocumentArrowUpSolid from '$lib/components/icons/DocumentArrowUpSolid.svelte';
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte';
|
import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte';
|
||||||
import { config } from '$lib/stores';
|
import { config } from '$lib/stores';
|
||||||
|
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let uploadFilesHandler: Function;
|
export let uploadFilesHandler: Function;
|
||||||
|
|
||||||
|
export let selectedToolIds: string[] = [];
|
||||||
export let webSearchEnabled: boolean;
|
export let webSearchEnabled: boolean;
|
||||||
|
|
||||||
|
export let tools = {};
|
||||||
export let onClose: Function;
|
export let onClose: Function;
|
||||||
|
|
||||||
let show = false;
|
let show = false;
|
||||||
|
|
@ -46,6 +45,32 @@
|
||||||
align="start"
|
align="start"
|
||||||
transition={flyAndScale}
|
transition={flyAndScale}
|
||||||
>
|
>
|
||||||
|
{#if Object.keys(tools).length > 0}
|
||||||
|
{#each Object.keys(tools) as toolId}
|
||||||
|
<div
|
||||||
|
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
|
||||||
|
>
|
||||||
|
<div class="flex-1 flex items-center gap-2">
|
||||||
|
<WrenchSolid />
|
||||||
|
|
||||||
|
<Tooltip content={tools[toolId]?.description ?? ''}>
|
||||||
|
<div class="flex items-center line-clamp-1">{tools[toolId].name}</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
bind:state={tools[toolId].enabled}
|
||||||
|
on:change={(e) => {
|
||||||
|
selectedToolIds = e.detail
|
||||||
|
? [...selectedToolIds, toolId]
|
||||||
|
: selectedToolIds.filter((id) => id !== toolId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<hr class="border-gray-100 dark:border-gray-800 my-1" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if $config?.features?.enable_web_search}
|
{#if $config?.features?.enable_web_search}
|
||||||
<div
|
<div
|
||||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
|
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,10 @@
|
||||||
];
|
];
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
value = boilerplate;
|
console.log(value);
|
||||||
|
if (value === '') {
|
||||||
|
value = boilerplate;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if html class has dark mode
|
// Check if html class has dark mode
|
||||||
isDarkMode = document.documentElement.classList.contains('dark');
|
isDarkMode = document.documentElement.classList.contains('dark');
|
||||||
|
|
@ -107,27 +110,24 @@
|
||||||
attributeFilter: ['class']
|
attributeFilter: ['class']
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add a keyboard shortcut to format the code when Ctrl/Cmd + S is pressed
|
const keydownHandler = async (e) => {
|
||||||
// Override the default browser save functionality
|
|
||||||
|
|
||||||
const handleSave = async (e) => {
|
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const res = await formatPythonCodeHandler().catch((error) => {
|
dispatch('save');
|
||||||
return null;
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (res) {
|
// Format code when Ctrl + Shift + F is pressed
|
||||||
dispatch('save');
|
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'f') {
|
||||||
}
|
e.preventDefault();
|
||||||
|
await formatPythonCodeHandler();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('keydown', handleSave);
|
document.addEventListener('keydown', keydownHandler);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
document.removeEventListener('keydown', handleSave);
|
document.removeEventListener('keydown', keydownHandler);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
11
src/lib/components/icons/WrenchSolid.svelte
Normal file
11
src/lib/components/icons/WrenchSolid.svelte
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let className = 'size-4';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class={className}>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M12 6.75a5.25 5.25 0 0 1 6.775-5.025.75.75 0 0 1 .313 1.248l-3.32 3.319c.063.475.276.934.641 1.299.365.365.824.578 1.3.64l3.318-3.319a.75.75 0 0 1 1.248.313 5.25 5.25 0 0 1-5.472 6.756c-1.018-.086-1.87.1-2.309.634L7.344 21.3A3.298 3.298 0 1 1 2.7 16.657l8.684-7.151c.533-.44.72-1.291.634-2.309A5.342 5.342 0 0 1 12 6.75ZM4.117 19.125a.75.75 0 0 1 .75-.75h.008a.75.75 0 0 1 .75.75v.008a.75.75 0 0 1-.75.75h-.008a.75.75 0 0 1-.75-.75v-.008Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
@ -4,12 +4,23 @@
|
||||||
const { saveAs } = fileSaver;
|
const { saveAs } = fileSaver;
|
||||||
|
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
import { WEBUI_NAME, prompts } from '$lib/stores';
|
import { WEBUI_NAME, prompts, tools } from '$lib/stores';
|
||||||
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
|
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
|
||||||
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import {
|
||||||
|
createNewTool,
|
||||||
|
deleteToolById,
|
||||||
|
exportTools,
|
||||||
|
getToolById,
|
||||||
|
getTools
|
||||||
|
} from '$lib/apis/tools';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
let toolsImportInputElement: HTMLInputElement;
|
||||||
|
let importFiles;
|
||||||
|
|
||||||
let query = '';
|
let query = '';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -65,3 +76,216 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class=" dark:border-gray-850 my-2.5" />
|
<hr class=" dark:border-gray-850 my-2.5" />
|
||||||
|
|
||||||
|
<div class="my-3 mb-5">
|
||||||
|
{#each $tools.filter((t) => query === '' || t.name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(query.toLowerCase()) || t.id.toLowerCase().includes(query.toLowerCase())) as tool}
|
||||||
|
<button
|
||||||
|
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
goto(`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
|
||||||
|
<a
|
||||||
|
href={`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`}
|
||||||
|
class="flex items-center text-left"
|
||||||
|
>
|
||||||
|
<div class=" flex-1 self-center pl-5">
|
||||||
|
<div class=" font-semibold flex items-center gap-1.5">
|
||||||
|
<div>
|
||||||
|
{tool.name}
|
||||||
|
</div>
|
||||||
|
<div class=" text-gray-500 text-xs font-medium">{tool.id}</div>
|
||||||
|
</div>
|
||||||
|
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
|
||||||
|
{tool.meta.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row space-x-1 self-center">
|
||||||
|
<a
|
||||||
|
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||||
|
type="button"
|
||||||
|
href={`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||||
|
type="button"
|
||||||
|
on:click={async () => {
|
||||||
|
const _tool = await getToolById(localStorage.token, tool.id).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_tool) {
|
||||||
|
sessionStorage.tool = JSON.stringify({
|
||||||
|
..._tool,
|
||||||
|
id: `${_tool.id}_clone`,
|
||||||
|
name: `${_tool.name} (Clone)`
|
||||||
|
});
|
||||||
|
goto('/workspace/tools/create');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||||
|
type="button"
|
||||||
|
on:click={async () => {
|
||||||
|
const res = await deleteToolById(localStorage.token, tool.id).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
toast.success('Tool deleted successfully');
|
||||||
|
tools.set(await getTools(localStorage.token));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" flex justify-end w-full mb-2">
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<input
|
||||||
|
id="documents-import-input"
|
||||||
|
bind:this={toolsImportInputElement}
|
||||||
|
bind:files={importFiles}
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
hidden
|
||||||
|
on:change={() => {
|
||||||
|
console.log(importFiles);
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (event) => {
|
||||||
|
const _tools = JSON.parse(event.target.result);
|
||||||
|
console.log(_tools);
|
||||||
|
|
||||||
|
for (const tool of _tools) {
|
||||||
|
const res = await createNewTool(localStorage.token, tool).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Tool imported successfully');
|
||||||
|
tools.set(await getTools(localStorage.token));
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsText(importFiles[0]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
|
||||||
|
on:click={() => {
|
||||||
|
toolsImportInputElement.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" self-center mr-2 font-medium">{$i18n.t('Import Tools')}</div>
|
||||||
|
|
||||||
|
<div class=" self-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
|
||||||
|
on:click={async () => {
|
||||||
|
const _tools = await exportTools(localStorage.token).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_tools) {
|
||||||
|
let blob = new Blob([JSON.stringify(_tools)], {
|
||||||
|
type: 'application/json'
|
||||||
|
});
|
||||||
|
saveAs(blob, `tools-export-${Date.now()}.json`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" self-center mr-2 font-medium">{$i18n.t('Export Tools')}</div>
|
||||||
|
|
||||||
|
<div class=" self-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
|
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let saveHandler: Function;
|
|
||||||
export let value = '';
|
export let value = '';
|
||||||
|
|
||||||
let codeEditor;
|
let codeEditor;
|
||||||
|
let boilerplate = `import os
|
||||||
let boilerplate = `# Tip: Use Ctrl/Cmd + S to format the code
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
import requests
|
import requests
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
class Tools:
|
class Tools:
|
||||||
|
|
@ -20,6 +20,20 @@ class Tools:
|
||||||
# Use Sphinx-style docstrings to document your tools, they will be used for generating tools specifications
|
# Use Sphinx-style docstrings to document your tools, they will be used for generating tools specifications
|
||||||
# Please refer to function_calling_filter_pipeline.py file from pipelines project for an example
|
# Please refer to function_calling_filter_pipeline.py file from pipelines project for an example
|
||||||
|
|
||||||
|
def get_environment_variable(self, variable_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Get the value of an environment variable.
|
||||||
|
:param variable_name: The name of the environment variable.
|
||||||
|
:return: The value of the environment variable or a message if it doesn't exist.
|
||||||
|
"""
|
||||||
|
value = os.getenv(variable_name)
|
||||||
|
if value is not None:
|
||||||
|
return (
|
||||||
|
f"The value of the environment variable '{variable_name}' is '{value}'"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return f"The environment variable '{variable_name}' does not exist."
|
||||||
|
|
||||||
def get_current_time(self) -> str:
|
def get_current_time(self) -> str:
|
||||||
"""
|
"""
|
||||||
Get the current time.
|
Get the current time.
|
||||||
|
|
@ -45,6 +59,41 @@ class Tools:
|
||||||
print(e)
|
print(e)
|
||||||
return "Invalid equation"
|
return "Invalid equation"
|
||||||
|
|
||||||
|
def get_current_weather(self, city: str) -> str:
|
||||||
|
"""
|
||||||
|
Get the current weather for a given city.
|
||||||
|
:param city: The name of the city to get the weather for.
|
||||||
|
:return: The current weather information or an error message.
|
||||||
|
"""
|
||||||
|
api_key = os.getenv("OPENWEATHER_API_KEY")
|
||||||
|
if not api_key:
|
||||||
|
return (
|
||||||
|
"API key is not set in the environment variable 'OPENWEATHER_API_KEY'."
|
||||||
|
)
|
||||||
|
|
||||||
|
base_url = "http://api.openweathermap.org/data/2.5/weather"
|
||||||
|
params = {
|
||||||
|
"q": city,
|
||||||
|
"appid": api_key,
|
||||||
|
"units": "metric", # Optional: Use 'imperial' for Fahrenheit
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(base_url, params=params)
|
||||||
|
response.raise_for_status() # Raise HTTPError for bad responses (4xx and 5xx)
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
if data.get("cod") != 200:
|
||||||
|
return f"Error fetching weather data: {data.get('message')}"
|
||||||
|
|
||||||
|
weather_description = data["weather"][0]["description"]
|
||||||
|
temperature = data["main"]["temp"]
|
||||||
|
humidity = data["main"]["humidity"]
|
||||||
|
wind_speed = data["wind"]["speed"]
|
||||||
|
|
||||||
|
return f"Weather in {city}: {temperature}°C"
|
||||||
|
except requests.RequestException as e:
|
||||||
|
return f"Error fetching weather data: {str(e)}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const formatHandler = async () => {
|
export const formatHandler = async () => {
|
||||||
|
|
@ -60,6 +109,6 @@ class Tools:
|
||||||
{boilerplate}
|
{boilerplate}
|
||||||
bind:this={codeEditor}
|
bind:this={codeEditor}
|
||||||
on:save={() => {
|
on:save={() => {
|
||||||
saveHandler();
|
dispatch('save');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,27 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from 'svelte';
|
import { getContext, createEventDispatcher, onMount } from 'svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
import CodeEditor from './CodeEditor.svelte';
|
import CodeEditor from './CodeEditor.svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let formElement = null;
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
|
||||||
let id = '';
|
export let edit = false;
|
||||||
let name = '';
|
export let clone = false;
|
||||||
let meta = {
|
|
||||||
|
export let id = '';
|
||||||
|
export let name = '';
|
||||||
|
export let meta = {
|
||||||
description: ''
|
description: ''
|
||||||
};
|
};
|
||||||
|
export let content = '';
|
||||||
|
|
||||||
let code = '';
|
$: if (name && !edit && !clone) {
|
||||||
|
|
||||||
$: if (name) {
|
|
||||||
id = name.replace(/\s+/g, '_').toLowerCase();
|
id = name.replace(/\s+/g, '_').toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -24,8 +29,12 @@
|
||||||
|
|
||||||
const saveHandler = async () => {
|
const saveHandler = async () => {
|
||||||
loading = true;
|
loading = true;
|
||||||
// Call the API to save the toolkit
|
dispatch('save', {
|
||||||
console.log('saveHandler');
|
id,
|
||||||
|
name,
|
||||||
|
meta,
|
||||||
|
content
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitHandler = async () => {
|
const submitHandler = async () => {
|
||||||
|
|
@ -42,13 +51,20 @@
|
||||||
|
|
||||||
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
|
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
|
||||||
<div class="mx-auto w-full md:px-0 h-full">
|
<div class="mx-auto w-full md:px-0 h-full">
|
||||||
<div class=" flex flex-col max-h-[100dvh] h-full">
|
<form
|
||||||
|
bind:this={formElement}
|
||||||
|
class=" flex flex-col max-h-[100dvh] h-full"
|
||||||
|
on:submit|preventDefault={() => {
|
||||||
|
submitHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div class="mb-2.5">
|
<div class="mb-2.5">
|
||||||
<button
|
<button
|
||||||
class="flex space-x-1"
|
class="flex space-x-1"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
goto('/workspace/tools');
|
goto('/workspace/tools');
|
||||||
}}
|
}}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<div class=" self-center">
|
<div class=" self-center">
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -80,11 +96,12 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
|
class="w-full px-3 py-2 text-sm font-medium disabled:text-gray-300 dark:disabled:text-gray-700 bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Toolkit ID (e.g. my_toolkit)"
|
placeholder="Toolkit ID (e.g. my_toolkit)"
|
||||||
bind:value={id}
|
bind:value={id}
|
||||||
required
|
required
|
||||||
|
disabled={edit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
|
|
@ -97,20 +114,25 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
|
<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
|
||||||
<CodeEditor bind:value={code} bind:this={codeEditor} {saveHandler} />
|
<CodeEditor
|
||||||
|
bind:value={content}
|
||||||
|
bind:this={codeEditor}
|
||||||
|
on:save={() => {
|
||||||
|
if (formElement) {
|
||||||
|
formElement.requestSubmit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pb-3 flex justify-end">
|
<div class="pb-3 flex justify-end">
|
||||||
<button
|
<button
|
||||||
class="px-3 py-1.5 text-sm font-medium bg-emerald-600 hover:bg-emerald-700 text-gray-50 transition rounded-lg"
|
class="px-3 py-1.5 text-sm font-medium bg-emerald-600 hover:bg-emerald-700 text-gray-50 transition rounded-lg"
|
||||||
on:click={() => {
|
|
||||||
submitHandler();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
{$i18n.t('Save')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -23,24 +23,11 @@ export const chatId = writable('');
|
||||||
|
|
||||||
export const chats = writable([]);
|
export const chats = writable([]);
|
||||||
export const tags = writable([]);
|
export const tags = writable([]);
|
||||||
export const models: Writable<Model[]> = writable([]);
|
|
||||||
|
|
||||||
export const modelfiles = writable([]);
|
export const models: Writable<Model[]> = writable([]);
|
||||||
export const prompts: Writable<Prompt[]> = writable([]);
|
export const prompts: Writable<Prompt[]> = writable([]);
|
||||||
export const documents = writable([
|
export const documents: Writable<Document[]> = writable([]);
|
||||||
{
|
export const tools = writable([]);
|
||||||
collection_name: 'collection_name',
|
|
||||||
filename: 'filename',
|
|
||||||
name: 'name',
|
|
||||||
title: 'title'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
collection_name: 'collection_name1',
|
|
||||||
filename: 'filename1',
|
|
||||||
name: 'name1',
|
|
||||||
title: 'title1'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const banners: Writable<Banner[]> = writable([]);
|
export const banners: Writable<Banner[]> = writable([]);
|
||||||
|
|
||||||
|
|
@ -135,6 +122,13 @@ type Prompt = {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Document = {
|
||||||
|
collection_name: string;
|
||||||
|
filename: string;
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
type Config = {
|
type Config = {
|
||||||
status: boolean;
|
status: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,15 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
import { getModels as _getModels } from '$lib/apis';
|
import { getModels as _getModels } from '$lib/apis';
|
||||||
import { getOllamaVersion } from '$lib/apis/ollama';
|
|
||||||
import { getPrompts } from '$lib/apis/prompts';
|
|
||||||
|
|
||||||
import { getDocs } from '$lib/apis/documents';
|
|
||||||
import { getAllChatTags } from '$lib/apis/chats';
|
import { getAllChatTags } from '$lib/apis/chats';
|
||||||
|
|
||||||
|
import { getPrompts } from '$lib/apis/prompts';
|
||||||
|
import { getDocs } from '$lib/apis/documents';
|
||||||
|
import { getTools } from '$lib/apis/tools';
|
||||||
|
|
||||||
|
import { getBanners } from '$lib/apis/configs';
|
||||||
|
import { getUserSettings } from '$lib/apis/users';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
user,
|
user,
|
||||||
showSettings,
|
showSettings,
|
||||||
|
|
@ -25,33 +28,21 @@
|
||||||
banners,
|
banners,
|
||||||
showChangelog,
|
showChangelog,
|
||||||
config,
|
config,
|
||||||
showCallOverlay
|
showCallOverlay,
|
||||||
|
tools
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import { REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants';
|
|
||||||
import { compareVersion } from '$lib/utils';
|
|
||||||
|
|
||||||
import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
|
import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
|
||||||
import Sidebar from '$lib/components/layout/Sidebar.svelte';
|
import Sidebar from '$lib/components/layout/Sidebar.svelte';
|
||||||
import ShortcutsModal from '$lib/components/chat/ShortcutsModal.svelte';
|
|
||||||
import ChangelogModal from '$lib/components/ChangelogModal.svelte';
|
import ChangelogModal from '$lib/components/ChangelogModal.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
|
||||||
import { getBanners } from '$lib/apis/configs';
|
|
||||||
import { getUserSettings } from '$lib/apis/users';
|
|
||||||
import Help from '$lib/components/layout/Help.svelte';
|
|
||||||
import AccountPending from '$lib/components/layout/Overlay/AccountPending.svelte';
|
import AccountPending from '$lib/components/layout/Overlay/AccountPending.svelte';
|
||||||
import { error } from '@sveltejs/kit';
|
|
||||||
import CallOverlay from '$lib/components/chat/MessageInput/CallOverlay.svelte';
|
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
let ollamaVersion = '';
|
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
let showShortcutsButtonElement: HTMLButtonElement;
|
|
||||||
let DB = null;
|
let DB = null;
|
||||||
let localDBChats = [];
|
let localDBChats = [];
|
||||||
|
|
||||||
let showShortcuts = false;
|
|
||||||
|
|
||||||
const getModels = async () => {
|
const getModels = async () => {
|
||||||
return _getModels(localStorage.token);
|
return _getModels(localStorage.token);
|
||||||
};
|
};
|
||||||
|
|
@ -99,6 +90,9 @@
|
||||||
(async () => {
|
(async () => {
|
||||||
documents.set(await getDocs(localStorage.token));
|
documents.set(await getDocs(localStorage.token));
|
||||||
})(),
|
})(),
|
||||||
|
(async () => {
|
||||||
|
tools.set(await getTools(localStorage.token));
|
||||||
|
})(),
|
||||||
(async () => {
|
(async () => {
|
||||||
banners.set(await getBanners(localStorage.token));
|
banners.set(await getBanners(localStorage.token));
|
||||||
})(),
|
})(),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,52 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { createNewTool, getTools } from '$lib/apis/tools';
|
||||||
import ToolkitEditor from '$lib/components/workspace/Tools/ToolkitEditor.svelte';
|
import ToolkitEditor from '$lib/components/workspace/Tools/ToolkitEditor.svelte';
|
||||||
|
import { tools } from '$lib/stores';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
let clone = false;
|
||||||
|
let tool = null;
|
||||||
|
|
||||||
|
const saveHandler = async (data) => {
|
||||||
|
console.log(data);
|
||||||
|
const res = await createNewTool(localStorage.token, {
|
||||||
|
id: data.id,
|
||||||
|
name: data.name,
|
||||||
|
meta: data.meta,
|
||||||
|
content: data.content
|
||||||
|
}).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
toast.success('Tool created successfully');
|
||||||
|
tools.set(await getTools(localStorage.token));
|
||||||
|
|
||||||
|
await goto('/workspace/tools');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
console.log('mounted');
|
||||||
|
|
||||||
|
if (sessionStorage.tool) {
|
||||||
|
tool = JSON.parse(sessionStorage.tool);
|
||||||
|
sessionStorage.removeItem('tool');
|
||||||
|
clone = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ToolkitEditor />
|
<ToolkitEditor
|
||||||
|
id={tool?.id ?? ''}
|
||||||
|
name={tool?.name ?? ''}
|
||||||
|
meta={tool?.meta ?? { description: '' }}
|
||||||
|
content={tool?.content ?? ''}
|
||||||
|
{clone}
|
||||||
|
on:save={(e) => {
|
||||||
|
saveHandler(e.detail);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,66 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { getToolById, getTools, updateToolById } from '$lib/apis/tools';
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import ToolkitEditor from '$lib/components/workspace/Tools/ToolkitEditor.svelte';
|
import ToolkitEditor from '$lib/components/workspace/Tools/ToolkitEditor.svelte';
|
||||||
|
import { tools } from '$lib/stores';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
let tool = null;
|
||||||
|
|
||||||
|
const saveHandler = async (data) => {
|
||||||
|
console.log(data);
|
||||||
|
const res = await updateToolById(localStorage.token, tool.id, {
|
||||||
|
id: data.id,
|
||||||
|
name: data.name,
|
||||||
|
meta: data.meta,
|
||||||
|
content: data.content
|
||||||
|
}).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
toast.success('Tool updated successfully');
|
||||||
|
tools.set(await getTools(localStorage.token));
|
||||||
|
|
||||||
|
await goto('/workspace/tools');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
console.log('mounted');
|
||||||
|
const id = $page.url.searchParams.get('id');
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
tool = await getToolById(localStorage.token, id).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
goto('/workspace/tools');
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(tool);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ToolkitEditor />
|
{#if tool}
|
||||||
|
<ToolkitEditor
|
||||||
|
edit={true}
|
||||||
|
id={tool.id}
|
||||||
|
name={tool.name}
|
||||||
|
meta={tool.meta}
|
||||||
|
content={tool.content}
|
||||||
|
on:save={(e) => {
|
||||||
|
saveHandler(e.detail);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="flex items-center justify-center h-full">
|
||||||
|
<div class=" pb-16">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue