Files
ollama-plus/tools-export-1757781877851.json
T
2025-09-13 12:46:39 -04:00

1 line
137 KiB
JSON

[{"id":"followup_and_scheduled_tasks_controller","user_id":"b0dbb398-2e9d-406c-af2f-2843a5a83848","name":"Followup and Scheduled Tasks Controller","content":"# all comments are lowercase\nimport os, json, httpx\nfrom typing import Optional, Dict, Any, List\nfrom pydantic import BaseModel, Field\n\n\nclass Tools:\n class Valves(BaseModel):\n API_BASE_URL: str = Field(\n default=\"http://ollama-scheduler:12253\", description=\"the scheduler api url\"\n )\n\n def __init__(self):\n # open webui populates valves for tools ig, so just keep local handle\n self.valves = self.Valves()\n\n # ---------- helpers ----------\n\n @staticmethod\n def _ensure_user_id(__user__: Optional[dict]) -> str:\n uid = (__user__ or {}).get(\"id\")\n if not uid:\n raise ValueError(\"error: no user context available\")\n return str(uid)\n\n @staticmethod\n def _derive_context(\n context: Optional[str], __messages__: Optional[List[dict]]\n ) -> str:\n if context:\n return context\n if __messages__:\n for m in reversed(__messages__):\n if m.get(\"role\") == \"user\":\n return str(m.get(\"content\", \"\"))[:4000] # keep it sane\n return \"\"\n\n @staticmethod\n def _collect_file_urls(__files__: Optional[List[dict]]) -> List[str]:\n urls: List[str] = []\n if __files__:\n for f in __files__:\n u = f.get(\"url\")\n if u:\n urls.append(str(u))\n return urls\n\n def _headers(self, user_id: str) -> Dict[str, str]:\n # every call is scoped to the caller via this header\n return {\"content-type\": \"application/json\", \"x-user-id\": user_id}\n\n # ---------- core calls to your scheduler api ----------\n\n async def list_workflow_templates(self) -> str:\n async with httpx.AsyncClient(timeout=30) as client:\n r = await client.get(f\"{self.valves.API_BASE_URL}/api/workflowtemplates\")\n r.raise_for_status()\n return r.text\n\n async def list_schedules(self, __user__: Optional[dict] = None) -> str:\n user_id = self._ensure_user_id(__user__)\n async with httpx.AsyncClient(timeout=30) as client:\n r = await client.get(\n f\"{self.valves.API_BASE_URL}/api/schedules\",\n headers=self._headers(user_id),\n )\n r.raise_for_status()\n return r.text\n\n async def delete_schedule(self, name: str, __user__: Optional[dict] = None) -> str:\n user_id = self._ensure_user_id(__user__)\n async with httpx.AsyncClient(timeout=30) as client:\n r = await client.delete(\n f\"{self.valves.API_BASE_URL}/schedules/{httpx.QueryParams({'n': name})['n']}\",\n headers=self._headers(user_id),\n )\n # the api returns 204 on success\n if r.status_code not in (200, 202, 204):\n r.raise_for_status()\n return json.dumps({\"ok\": r.status_code in (200, 202, 204)})\n\n async def schedule_job(\n self,\n # schedule identity / timing\n name: str,\n when_iso: Optional[str] = None,\n when_cron: Optional[str] = None,\n tz: str = \"America/New_York\",\n one_shot: bool = False,\n # argo template wiring\n template_name: str = \"\",\n cluster_scope: bool = False,\n entrypoint: Optional[str] = None,\n # content to pass through\n query: Optional[str] = None,\n context: Optional[str] = None,\n extra_parameters: Optional[Dict[str, Any]] = None,\n __user__: Optional[dict] = None,\n __files__: Optional[list] = None,\n __messages__: Optional[list] = None,\n ) -> str:\n user_id = self._ensure_user_id(__user__)\n ctx = self._derive_context(context, __messages__)\n file_urls = self._collect_file_urls(__files__)\n\n params: Dict[str, Any] = {\"query\": query or \"\", \"context\": ctx}\n if file_urls:\n params[\"file_urls\"] = file_urls\n if extra_parameters:\n params.update(extra_parameters)\n\n body = {\n \"name\": name,\n \"when\": (\n {\"iso\": when_iso} if when_iso else {\"cron\": when_cron or \"* * * * *\"}\n ),\n \"tz\": tz,\n \"oneShot\": bool(one_shot),\n \"template\": {\"name\": template_name, \"clusterScope\": bool(cluster_scope)},\n \"entrypoint\": entrypoint,\n \"parameters\": params,\n }\n\n async with httpx.AsyncClient(timeout=60) as client:\n r = await client.post(\n f\"{self.valves.API_BASE_URL}/schedules\",\n headers=self._headers(user_id),\n content=json.dumps(body),\n )\n r.raise_for_status()\n return r.text\n\n async def run_now(\n self,\n name: str,\n template_name: str,\n cluster_scope: bool = False,\n entrypoint: Optional[str] = None,\n query: Optional[str] = None,\n context: Optional[str] = None,\n extra_parameters: Optional[Dict[str, Any]] = None,\n __user__: Optional[dict] = None,\n __files__: Optional[list] = None,\n __messages__: Optional[list] = None,\n ) -> str:\n user_id = self._ensure_user_id(__user__)\n ctx = self._derive_context(context, __messages__)\n file_urls = self._collect_file_urls(__files__)\n\n params: Dict[str, Any] = {\"query\": query or \"\", \"context\": ctx}\n if file_urls:\n params[\"file_urls\"] = file_urls\n if extra_parameters:\n params.update(extra_parameters)\n\n body = {\n \"name\": name,\n \"template\": {\"name\": template_name, \"clusterScope\": bool(cluster_scope)},\n \"entrypoint\": entrypoint,\n \"parameters\": params,\n }\n\n async with httpx.AsyncClient(timeout=60) as client:\n r = await client.post(\n f\"{self.valves.API_BASE_URL}/run-now\",\n headers=self._headers(user_id),\n content=json.dumps(body),\n )\n r.raise_for_status()\n return r.text\n\n # # ---------- backwards-compatible shortcut (calls run-now) ----------\n\n # async def submit_job(\n # self,\n # query: str,\n # context: Optional[str] = None,\n # template_name: str = \"\",\n # cluster_scope: bool = False,\n # entrypoint: Optional[str] = None,\n # extra_parameters: Optional[Dict[str, Any]] = None,\n # __user__: Optional[dict] = None,\n # __files__: Optional[list] = None,\n # __messages__: Optional[list] = None,\n # ) -> str:\n # \"\"\"\n # legacy helper: run ad-hoc with query/context/files.\n # prefer run_now() or schedule_job() for fine control.\n # \"\"\"\n # # default name, server will add a unique suffix\n # name = \"adhoc\"\n\n # return await self.run_now(\n # name=name,\n # template_name=template_name,\n # cluster_scope=cluster_scope,\n # entrypoint=entrypoint,\n # query=query,\n # context=context,\n # extra_parameters=extra_parameters,\n # __user__=__user__,\n # __files__=__files__,\n # __messages__=__messages__,\n # )\n","specs":[{"name":"_collect_file_urls","description":"","parameters":{"properties":{},"type":"object"}},{"name":"_derive_context","description":"","parameters":{"properties":{"context":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["context"],"type":"object"}},{"name":"_ensure_user_id","description":"","parameters":{"properties":{},"type":"object"}},{"name":"_headers","description":"","parameters":{"properties":{"user_id":{"type":"string"}},"required":["user_id"],"type":"object"}},{"name":"delete_schedule","description":"","parameters":{"properties":{"name":{"type":"string"}},"required":["name"],"type":"object"}},{"name":"list_schedules","description":"","parameters":{"properties":{},"type":"object"}},{"name":"list_workflow_templates","description":"","parameters":{"properties":{},"type":"object"}},{"name":"run_now","description":"","parameters":{"properties":{"name":{"type":"string"},"template_name":{"type":"string"},"cluster_scope":{"default":false,"type":"boolean"},"entrypoint":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null},"query":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null},"context":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null},"extra_parameters":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"default":null}},"required":["name","template_name"],"type":"object"}},{"name":"schedule_job","description":"","parameters":{"properties":{"name":{"type":"string"},"when_iso":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null},"when_cron":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null},"tz":{"default":"America/New_York","type":"string"},"one_shot":{"default":false,"type":"boolean"},"template_name":{"default":"","type":"string"},"cluster_scope":{"default":false,"type":"boolean"},"entrypoint":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null},"query":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null},"context":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null},"extra_parameters":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"default":null}},"required":["name"],"type":"object"}}],"meta":{"description":"Schedule a task to be run later","manifest":{}},"access_control":{},"updated_at":1757781804,"created_at":1757781043},{"id":"knowledgebase_tools","user_id":"b0dbb398-2e9d-406c-af2f-2843a5a83848","name":"File Storage System Tools","content":"\"\"\"\ntitle: Knowledgebase File Tools\nauthor: Wes Caldwell\nemail: musicheardworldwide.com\nauthor_url: https://github.com/musicheardworldwide\nversion: 0.2.0\ndescription: A tool for managing and rendering files with expanded features for better usability.\n\"\"\"\n\nimport os\nimport requests\nimport logging\nfrom datetime import datetime\nfrom typing import List, Dict, Optional, Callable\n\n# Configure logging\nlogging.basicConfig(\n level=logging.INFO, format=\"%(asctime)s - %(levelname)s - %(message)s\"\n)\n\n\nclass Tools:\n def __init__(self):\n \"\"\"\n Initializes the Tools class with default settings.\n \"\"\"\n # Enable or disable specific features\n self.file_handler = True\n self.citation = True\n\n def get_files(self, __files__: List[dict] = []) -> str:\n \"\"\"\n Fetch the list of files and generate rendering instructions.\n \"\"\"\n logging.info(\"Fetching file list.\")\n return (\n \"\"\"Show the file content directly using: `/api/v1/files/{file_id}/content`\nIf the file is video content render the video directly using the following template: {{VIDEO_FILE_ID_[file_id]}}\nIf the file is HTML file render the HTML directly as iframe using the following template: {{HTML_FILE_ID_[file_id]}}\"\"\"\n + f\"\\nFiles: {str(__files__)}\"\n )\n\n def upload_file(self, file_path: str) -> Dict[str, str]:\n \"\"\"\n Upload a file to the server.\n\n Parameters:\n - file_path (str): The path to the file to upload.\n\n Returns:\n - dict: The server response or an error message.\n \"\"\"\n url = \"/api/v1/files/\"\n headers = {\"Authorization\": \"Bearer YOUR_API_KEY\"}\n try:\n with open(file_path, \"rb\") as file:\n files = {\"file\": (os.path.basename(file_path), file)}\n response = requests.post(url, headers=headers, files=files)\n response.raise_for_status()\n logging.info(f\"File uploaded successfully: {file_path}\")\n return response.json()\n except Exception as e:\n logging.error(f\"Error uploading file: {str(e)}\")\n return {\"error\": str(e)}\n\n def delete_file(self, file_id: str) -> Dict[str, str]:\n \"\"\"\n Delete a file by its ID.\n\n Parameters:\n - file_id (str): The ID of the file to delete.\n\n Returns:\n - dict: The server response or an error message.\n \"\"\"\n url = f\"/api/v1/files/{file_id}\"\n headers = {\"Authorization\": \"Bearer YOUR_API_KEY\"}\n try:\n response = requests.delete(url, headers=headers)\n response.raise_for_status()\n logging.info(f\"File {file_id} deleted successfully.\")\n return {\n \"status\": \"success\",\n \"message\": f\"File {file_id} deleted successfully\",\n }\n except Exception as e:\n logging.error(f\"Error deleting file: {str(e)}\")\n return {\"error\": str(e)}\n\n def search_files(self, query: str) -> List[dict]:\n \"\"\"\n Search files by query.\n\n Parameters:\n - query (str): The search query.\n\n Returns:\n - list: A list of files matching the query or an error message.\n \"\"\"\n url = \"/api/v1/files/search\"\n params = {\"q\": query}\n headers = {\"Authorization\": \"Bearer YOUR_API_KEY\"}\n try:\n response = requests.get(url, headers=headers, params=params)\n response.raise_for_status()\n logging.info(f\"Search completed for query: {query}\")\n return response.json()\n except Exception as e:\n logging.error(f\"Error searching files: {str(e)}\")\n return [{\"error\": str(e)}]\n\n def render_file(self, file_id: str, file_type: str) -> str:\n \"\"\"\n Render files dynamically based on their type.\n\n Parameters:\n - file_id (str): The ID of the file.\n - file_type (str): The type of the file (e.g., video, html, pdf, text).\n\n Returns:\n - str: The rendering template.\n \"\"\"\n templates = {\n \"video\": f\"{{VIDEO_FILE_ID_{file_id}}}\",\n \"html\": f\"{{HTML_FILE_ID_{file_id}}}\",\n \"pdf\": f\"{{PDF_FILE_ID_{file_id}}}\",\n \"text\": f\"{{TEXT_FILE_ID_{file_id}}}\",\n }\n return templates.get(file_type, f\"Unsupported file type: {file_type}\")\n\n async def upload_with_progress(\n self, file_path: str, __event_emitter__: Callable[[dict], None]\n ):\n \"\"\"\n Upload a file with progress updates.\n\n Parameters:\n - file_path (str): The path to the file to upload.\n - __event_emitter__ (Callable): An event emitter for sending real-time updates.\n \"\"\"\n await __event_emitter__({\"status\": \"start\", \"description\": \"Uploading file...\"})\n result = self.upload_file(file_path)\n if \"error\" in result:\n await __event_emitter__({\"status\": \"error\", \"description\": result[\"error\"]})\n else:\n await __event_emitter__(\n {\"status\": \"success\", \"description\": \"File uploaded successfully.\"}\n )\n\n def add_to_collection(self, collection_id: str, file_id: str) -> Dict[str, str]:\n \"\"\"\n Add a file to a collection.\n\n Parameters:\n - collection_id (str): The ID of the collection.\n - file_id (str): The ID of the file.\n\n Returns:\n - dict: The server response or an error message.\n \"\"\"\n url = f\"/api/v1/knowledge/{collection_id}/file/add\"\n headers = {\"Authorization\": \"Bearer YOUR_API_KEY\"}\n try:\n response = requests.post(url, headers=headers, json={\"file_id\": file_id})\n response.raise_for_status()\n logging.info(f\"File {file_id} added to collection {collection_id}.\")\n return response.json()\n except Exception as e:\n logging.error(f\"Error adding file to collection: {str(e)}\")\n return {\"error\": str(e)}\n\n\n# Example usage\nif __name__ == \"__main__\":\n tools = Tools()\n files = tools.get_files([{\"id\": \"123\", \"name\": \"example.txt\"}])\n print(\"Available Files:\", files)\n\n upload_result = tools.upload_file(\"example.txt\")\n print(\"Upload Result:\", upload_result)\n\n search_result = tools.search_files(\"example\")\n print(\"Search Result:\", search_result)\n","specs":[{"name":"add_to_collection","description":"Add a file to a collection.\n\nParameters:\n- collection_id (str): The ID of the collection.\n- file_id (str): The ID of the file.\n\nReturns:\n- dict: The server response or an error message.","parameters":{"properties":{"collection_id":{"type":"string"},"file_id":{"type":"string"}},"required":["collection_id","file_id"],"type":"object"}},{"name":"delete_file","description":"Delete a file by its ID.\n\nParameters:\n- file_id (str): The ID of the file to delete.\n\nReturns:\n- dict: The server response or an error message.","parameters":{"properties":{"file_id":{"type":"string"}},"required":["file_id"],"type":"object"}},{"name":"get_files","description":"Fetch the list of files and generate rendering instructions.","parameters":{"properties":{},"type":"object"}},{"name":"render_file","description":"Render files dynamically based on their type.\n\nParameters:\n- file_id (str): The ID of the file.\n- file_type (str): The type of the file (e.g., video, html, pdf, text).\n\nReturns:\n- str: The rendering template.","parameters":{"properties":{"file_id":{"type":"string"},"file_type":{"type":"string"}},"required":["file_id","file_type"],"type":"object"}},{"name":"search_files","description":"Search files by query.\n\nParameters:\n- query (str): The search query.\n\nReturns:\n- list: A list of files matching the query or an error message.","parameters":{"properties":{"query":{"type":"string"}},"required":["query"],"type":"object"}},{"name":"upload_file","description":"Upload a file to the server.\n\nParameters:\n- file_path (str): The path to the file to upload.\n\nReturns:\n- dict: The server response or an error message.","parameters":{"properties":{"file_path":{"type":"string"}},"required":["file_path"],"type":"object"}},{"name":"upload_with_progress","description":"Upload a file with progress updates.\n\nParameters:\n- file_path (str): The path to the file to upload.\n- __event_emitter__ (Callable): An event emitter for sending real-time updates.","parameters":{"properties":{"file_path":{"type":"string"}},"required":["file_path"],"type":"object"}}],"meta":{"description":"A tool for managing and rendering files with expanded features for better usability.","manifest":{"title":"Knowledgebase File Tools","author":"Wes Caldwell","email":"musicheardworldwide.com","author_url":"https://github.com/musicheardworldwide","version":"0.2.0","description":"A tool for managing and rendering files with expanded features for better usability."}},"access_control":{},"updated_at":1757718928,"created_at":1757027713},{"id":"memory","user_id":"b0dbb398-2e9d-406c-af2f-2843a5a83848","name":"Memory","content":"\"\"\"\ntitle: Memory\nauthor: https://github.com/CookSleep\nversion: 0.0.1\nlicense: MIT\n\nThis tool supports a complete experience when using OpenAI API\n(and any API fully compatible with OpenAI API format) or Gemini models\nin native Function Calling mode.\n\nIf the API format is not supported, you can still use the default\nFunction Calling mode, but the experience will be significantly reduced.\n\nThis tool is an improved version of https://openwebui.com/t/mhio/met,\nfully utilizing Open WebUI's native memory functionality.\n\nYou don't need to enable the memory switch,\nas this tool only requires access to its database.\n\"\"\"\n\nimport json\nfrom typing import Callable, Any, List\n\nfrom open_webui.models.memories import Memories\nfrom pydantic import BaseModel, Field\n\n\nclass EventEmitter:\n def __init__(self, event_emitter: Callable[[dict], Any] = None):\n self.event_emitter = event_emitter\n\n async def emit(self, description=\"Unknown state\", status=\"in_progress\", done=False):\n \"\"\"\n Send a status event to the event emitter.\n\n :param description: Event description\n :param status: Event status\n :param done: Whether the event is complete\n \"\"\"\n if self.event_emitter:\n await self.event_emitter(\n {\n \"type\": \"status\",\n \"data\": {\n \"status\": status,\n \"description\": description,\n \"done\": done,\n },\n }\n )\n\n\n# Pydantic model for memory update operations\nclass MemoryUpdate(BaseModel):\n index: int = Field(..., description=\"Index of the memory entry (1-based)\")\n content: str = Field(..., description=\"Updated content for the memory\")\n\n\nclass Tools:\n \"\"\"\n Memory\n\n Use this tool to autonomously save/modify/query memories across conversations.\n\n IMPORTANT: Users rarely explicitly tell you what to remember!\n You must actively observe and identify important information that should be stored.\n\n Key features:\n 1. Proactive memory creation: Identify user preferences, project context, and recurring patterns\n 2. Intelligent memory usage: Reference stored information without requiring users to repeat themselves\n 3. Best practices: Store valuable information, maintain relevance, provide memories at appropriate times\n 4. Language matching: Always create memories in the user's preferred language and writing style\n\n IMPORTANT NOTE ON CLEARING MEMORIES:\n If a user asks to clear all memories, DO NOT attempt to implement this via code.\n Instead, inform them that clearing all memories is a high-risk operation that\n should be performed through their personal account settings panel using the\n \"Clear All Memories\" button. This prevents accidental data loss.\n \"\"\"\n\n class Valves(BaseModel):\n USE_MEMORY: bool = Field(\n default=True, description=\"Enable or disable memory usage.\"\n )\n DEBUG: bool = Field(default=True, description=\"Enable or disable debug mode.\")\n\n def __init__(self):\n \"\"\"Initialize the memory management tool.\"\"\"\n self.valves = self.Valves()\n\n async def recall_memories(\n self, __user__: dict = None, __event_emitter__: Callable[[dict], Any] = None\n ) -> str:\n \"\"\"\n Retrieves all stored memories from the user's memory vault.\n\n IMPORTANT: Proactively check memories to enhance your responses!\n Don't wait for users to ask what you remember.\n\n Returns memories in chronological order with index numbers.\n Use when you need to check stored information, reference previous\n preferences, or build context for responses.\n\n :param __user__: User dictionary containing the user ID\n :param __event_emitter__: Optional event emitter for tracking status\n :return: JSON string with indexed memories list\n \"\"\"\n emitter = EventEmitter(__event_emitter__)\n\n if not __user__:\n message = \"User ID not provided.\"\n await emitter.emit(description=message, status=\"missing_user_id\", done=True)\n return json.dumps({\"message\": message}, ensure_ascii=False)\n\n user_id = __user__.get(\"id\")\n if not user_id:\n message = \"User ID not provided.\"\n await emitter.emit(description=message, status=\"missing_user_id\", done=True)\n return json.dumps({\"message\": message}, ensure_ascii=False)\n\n await emitter.emit(\n description=\"Retrieving stored memories.\",\n status=\"recall_in_progress\",\n done=False,\n )\n\n user_memories = Memories.get_memories_by_user_id(user_id)\n if not user_memories:\n message = \"No memory stored.\"\n await emitter.emit(description=message, status=\"recall_complete\", done=True)\n return json.dumps({\"message\": message}, ensure_ascii=False)\n\n content_list = [\n f\"{index}. {memory.content}\"\n for index, memory in enumerate(\n sorted(user_memories, key=lambda m: m.created_at), start=1\n )\n ]\n\n await emitter.emit(\n description=f\"{len(user_memories)} memories loaded\",\n status=\"recall_complete\",\n done=True,\n )\n\n return f\"Memories from the users memory vault: {content_list}\"\n\n async def add_memory(\n self,\n input_text: List[\n str\n ], # Modified to only accept list, JSON Schema items.type is string\n __user__: dict = None,\n __event_emitter__: Callable[[dict], Any] = None,\n ) -> str:\n \"\"\"\n Adds one or more memories to the user's memory vault.\n\n IMPORTANT: Don't wait for explicit instructions to remember!\n Proactively identify and store important information.\n\n Good candidates for memories:\n - Personal preferences (favorite topics, entertainment, colors)\n - Professional information (field of expertise, current projects)\n - Important relationships (family, pets, close friends)\n - Recurring needs or requests (common questions, regular workflows)\n - Learning goals and interests (topics they're studying, skills they want to develop)\n\n Always use the user's preferred language and writing style.\n\n Memories should start with \"User\", for example:\n - \"User likes blue\"\n - \"User is a software engineer\"\n - \"User has a golden retriever named Max\"\n\n :param input_text: Single memory string or list of memory strings to store\n :param __user__: User dictionary containing the user ID\n :param __event_emitter__: Optional event emitter for tracking status\n :return: JSON string with result message\n \"\"\"\n emitter = EventEmitter(__event_emitter__)\n if not __user__:\n message = \"User ID not provided.\"\n await emitter.emit(description=message, status=\"missing_user_id\", done=True)\n return json.dumps({\"message\": message}, ensure_ascii=False)\n\n user_id = __user__.get(\"id\")\n if not user_id:\n message = \"User ID not provided.\"\n await emitter.emit(description=message, status=\"missing_user_id\", done=True)\n return json.dumps({\"message\": message}, ensure_ascii=False)\n\n # Handle single string input if needed\n if isinstance(input_text, str):\n input_text = [input_text]\n\n await emitter.emit(\n description=\"Adding entries to the memory vault.\",\n status=\"add_in_progress\",\n done=False,\n )\n\n # Process each memory item\n added_items = []\n failed_items = []\n\n for item in input_text:\n new_memory = Memories.insert_new_memory(user_id, item)\n if new_memory:\n added_items.append(item)\n else:\n failed_items.append(item)\n\n if not added_items:\n message = \"Failed to add any memories.\"\n await emitter.emit(description=message, status=\"add_failed\", done=True)\n return json.dumps({\"message\": message}, ensure_ascii=False)\n\n # Prepare result message\n added_count = len(added_items)\n failed_count = len(failed_items)\n\n if failed_count > 0:\n message = (\n f\"Added {added_count} memories, failed to add {failed_count} memories.\"\n )\n else:\n message = f\"Successfully added {added_count} memories.\"\n\n await emitter.emit(\n description=message,\n status=\"add_complete\",\n done=True,\n )\n return json.dumps({\"message\": message}, ensure_ascii=False)\n\n async def delete_memory(\n self,\n indices: List[int], # Modified to only accept list, items.type is integer\n __user__: dict = None,\n __event_emitter__: Callable[[dict], Any] = None,\n ) -> str:\n \"\"\"\n Delete one or more memory entries from the user's memory vault.\n\n Use to remove outdated or incorrect memories.\n\n For single deletion: provide an integer index\n For multiple deletions: provide a list of integer indices\n\n Indices refer to the position in the sorted list (1-based).\n\n :param indices: Single index (int) or list of indices to delete\n :param __user__: User dictionary containing the user ID\n :param __event_emitter__: Optional event emitter\n :return: JSON string with result message\n \"\"\"\n emitter = EventEmitter(__event_emitter__)\n\n if not __user__:\n message = \"User ID not provided.\"\n await emitter.emit(description=message, status=\"missing_user_id\", done=True)\n return json.dumps({\"message\": message}, ensure_ascii=False)\n\n user_id = __user__.get(\"id\")\n if not user_id:\n message = \"User ID not provided.\"\n await emitter.emit(description=message, status=\"missing_user_id\", done=True)\n return json.dumps({\"message\": message}, ensure_ascii=False)\n\n # Handle single integer input if needed\n if isinstance(indices, int):\n indices = [indices]\n\n await emitter.emit(\n description=f\"Deleting {len(indices)} memory entries.\",\n status=\"delete_in_progress\",\n done=False,\n )\n\n # Get all memories for this user\n user_memories = Memories.get_memories_by_user_id(user_id)\n if not user_memories:\n message = \"No memories found to delete.\"\n await emitter.emit(description=message, status=\"delete_failed\", done=True)\n return json.dumps({\"message\": message}, ensure_ascii=False)\n\n sorted_memories = sorted(user_memories, key=lambda m: m.created_at)\n responses = []\n\n for index in indices:\n if index < 1 or index > len(sorted_memories):\n message = f\"Memory index {index} does not exist.\"\n responses.append(message)\n await emitter.emit(\n description=message, status=\"delete_failed\", done=False\n )\n continue\n\n # Get the memory by index (1-based index)\n memory_to_delete = sorted_memories[index - 1]\n\n # Delete the memory\n result = Memories.delete_memory_by_id(memory_to_delete.id)\n if not result:\n message = f\"Failed to delete memory at index {index}.\"\n responses.append(message)\n await emitter.emit(\n description=message, status=\"delete_failed\", done=False\n )\n else:\n message = f\"Memory at index {index} deleted successfully.\"\n responses.append(message)\n await emitter.emit(\n description=message, status=\"delete_success\", done=False\n )\n\n await emitter.emit(\n description=\"All requested memory deletions have been processed.\",\n status=\"delete_complete\",\n done=True,\n )\n return json.dumps({\"message\": \"\\n\".join(responses)}, ensure_ascii=False)\n\n async def update_memory(\n self,\n updates: List[\n MemoryUpdate\n ], # Modified to accept list of MemoryUpdate objects, items.type is object\n __user__: dict = None,\n __event_emitter__: Callable[[dict], Any] = None,\n ) -> str:\n \"\"\"\n Update one or more memory entries in the user's memory vault.\n\n Use to modify existing memories when information changes.\n\n For single update: provide a dict with 'index' and 'content' keys\n For multiple updates: provide a list of dicts with 'index' and 'content' keys\n\n The 'index' refers to the position in the sorted list (1-based).\n\n Common scenarios: Correcting information, adding details,\n updating preferences, or refining wording.\n\n :param updates: Dict with 'index' and 'content' keys OR a list of such dicts\n :param __user__: User dictionary containing the user ID\n :param __event_emitter__: Optional event emitter\n :return: JSON string with result message\n \"\"\"\n emitter = EventEmitter(__event_emitter__)\n\n if not __user__:\n message = \"User ID not provided.\"\n await emitter.emit(description=message, status=\"missing_user_id\", done=True)\n return json.dumps({\"message\": message}, ensure_ascii=False)\n\n user_id = __user__.get(\"id\")\n if not user_id:\n message = \"User ID not provided.\"\n await emitter.emit(description=message, status=\"missing_user_id\", done=True)\n return json.dumps({\"message\": message}, ensure_ascii=False)\n\n await emitter.emit(\n description=f\"Updating {len(updates)} memory entries.\",\n status=\"update_in_progress\",\n done=False,\n )\n\n # Get all memories for this user\n user_memories = Memories.get_memories_by_user_id(user_id)\n if not user_memories:\n message = \"No memories found to update.\"\n await emitter.emit(description=message, status=\"update_failed\", done=True)\n return json.dumps({\"message\": message}, ensure_ascii=False)\n\n sorted_memories = sorted(user_memories, key=lambda m: m.created_at)\n responses = []\n\n for update_item in updates:\n # Convert dict to MemoryUpdate object if needed\n if isinstance(update_item, dict):\n try:\n update_item = MemoryUpdate.parse_obj(update_item)\n except Exception as e:\n message = f\"Invalid update item format: {update_item}\"\n responses.append(message)\n await emitter.emit(\n description=message, status=\"update_failed\", done=False\n )\n continue\n\n index = update_item.index\n content = update_item.content\n\n if index < 1 or index > len(sorted_memories):\n message = f\"Memory index {index} does not exist.\"\n responses.append(message)\n await emitter.emit(\n description=message, status=\"update_failed\", done=False\n )\n continue\n\n # Get the memory by index (1-based index)\n memory_to_update = sorted_memories[index - 1]\n\n # Update the memory\n updated_memory = Memories.update_memory_by_id(memory_to_update.id, content)\n if not updated_memory:\n message = f\"Failed to update memory at index {index}.\"\n responses.append(message)\n await emitter.emit(\n description=message, status=\"update_failed\", done=False\n )\n else:\n message = f\"Memory at index {index} updated successfully.\"\n responses.append(message)\n await emitter.emit(\n description=message, status=\"update_success\", done=False\n )\n\n await emitter.emit(\n description=\"All requested memory updates have been processed.\",\n status=\"update_complete\",\n done=True,\n )\n return json.dumps({\"message\": \"\\n\".join(responses)}, ensure_ascii=False)\n","specs":[{"name":"add_memory","description":"Adds one or more memories to the user's memory vault.\n\nIMPORTANT: Don't wait for explicit instructions to remember!\nProactively identify and store important information.\n\nGood candidates for memories:\n- Personal preferences (favorite topics, entertainment, colors)\n- Professional information (field of expertise, current projects)\n- Important relationships (family, pets, close friends)\n- Recurring needs or requests (common questions, regular workflows)\n- Learning goals and interests (topics they're studying, skills they want to develop)\n\nAlways use the user's preferred language and writing style.\n\nMemories should start with \"User\", for example:\n- \"User likes blue\"\n- \"User is a software engineer\"\n- \"User has a golden retriever named Max\"","parameters":{"properties":{"input_text":{"description":"Single memory string or list of memory strings to store","items":{"type":"string"},"type":"array"}},"required":["input_text"],"type":"object"}},{"name":"delete_memory","description":"Delete one or more memory entries from the user's memory vault.\n\nUse to remove outdated or incorrect memories.\n\nFor single deletion: provide an integer index\nFor multiple deletions: provide a list of integer indices\n\nIndices refer to the position in the sorted list (1-based).","parameters":{"properties":{"indices":{"description":"Single index (int) or list of indices to delete","items":{"type":"integer"},"type":"array"}},"required":["indices"],"type":"object"}},{"name":"recall_memories","description":"Retrieves all stored memories from the user's memory vault.\n\nIMPORTANT: Proactively check memories to enhance your responses!\nDon't wait for users to ask what you remember.\n\nReturns memories in chronological order with index numbers.\nUse when you need to check stored information, reference previous\npreferences, or build context for responses.","parameters":{"properties":{},"type":"object"}},{"name":"update_memory","description":"Update one or more memory entries in the user's memory vault.\n\nUse to modify existing memories when information changes.\n\nFor single update: provide a dict with 'index' and 'content' keys\nFor multiple updates: provide a list of dicts with 'index' and 'content' keys\n\nThe 'index' refers to the position in the sorted list (1-based).\n\nCommon scenarios: Correcting information, adding details,\nupdating preferences, or refining wording.","parameters":{"properties":{"updates":{"description":"Dict with 'index' and 'content' keys OR a list of such dicts","items":{"properties":{"index":{"description":"Index of the memory entry (1-based)","type":"integer"},"content":{"description":"Updated content for the memory","type":"string"}},"required":["index","content"],"type":"object"},"type":"array"}},"required":["updates"],"type":"object"}}],"meta":{"description":"Allow the model to autonomously save/modify/delete/read long-term memory.","manifest":{"title":"Memory","author":"https://github.com/CookSleep","version":"0.0.1","license":"MIT"}},"access_control":{},"updated_at":1757718836,"created_at":1757027758},{"id":"discord","user_id":"b0dbb398-2e9d-406c-af2f-2843a5a83848","name":"Discord","content":"\"\"\"\ntitle: Discord Webhook\nauthor: open-webui\nauthor_url: https://github.com/open-webui\nfunding_url: https://github.com/open-webui\nversion: 0.1.0\n\"\"\"\n\nimport os\nimport requests\nfrom datetime import datetime\nfrom pydantic import BaseModel, Field\n\n\nclass Tools:\n class Valves(BaseModel):\n WEBHOOK_URL: str = Field(\n default=\"\",\n description=\"The URL of the Discord webhook to send messages to.\",\n )\n\n def __init__(self):\n self.valves = self.Valves()\n pass\n\n def send_message(self, message_content: str) -> str:\n \"\"\"\n Send a message to a specified Discord channel using a webhook.\n\n :param message_content: The content of the message to be sent to the Discord channel.\n :return: None\n \"\"\"\n\n # Check if the webhook URL has been set\n if not self.valves.WEBHOOK_URL:\n return \"Let the user know webhook URL was not provided. Please configure the webhook URL.\"\n\n data = {\"content\": f\"{message_content} - Sent from Open WebUI\"}\n\n response = requests.post(self.valves.WEBHOOK_URL, json=data)\n\n if response.status_code == 204:\n return \"Message successfully sent, Let the user know the message has been sent.\"\n else:\n return f\"Failed to send message. HTTP Status Code: {response.status_code}, Let the user know there were some issues.\"\n","specs":[{"name":"send_message","description":"Send a message to a specified Discord channel using a webhook.","parameters":{"properties":{"message_content":{"description":"The content of the message to be sent to the Discord channel.","type":"string"}},"required":["message_content"],"type":"object"}}],"meta":{"description":"A tool for sending messages to a Discord channel using a webhook.","manifest":{"title":"Discord Webhook","author":"open-webui","author_url":"https://github.com/open-webui","funding_url":"https://github.com/open-webui","version":"0.1.0"}},"access_control":{},"updated_at":1757718511,"created_at":1757027728},{"id":"research","user_id":"b0dbb398-2e9d-406c-af2f-2843a5a83848","name":"Research","content":"\"\"\"\ntitle:Web Search using SRNXG with BeautifulSoup (Optimized Deep Research)\nauthor: Teodor Cucu (Credits to Jacob DeLacerda for giving me the Idea)\nversion: 0.2.3\ngithub: https://github.com/the-real-t30d0r/research-openwebui\nlicense: MIT\n\n\nYOU NEED THIS PROMPT IN ORDER TO USE IT: https://openwebui.com/p/t30d0r99/research\n\"\"\"\n\n#!/usr/bin/env python3\nimport os\nimport json\nfrom urllib.parse import urlparse\nimport re\nimport unicodedata\nfrom pydantic import BaseModel, Field\nimport asyncio\nimport aiohttp\nfrom typing import Any, Callable\nfrom bs4 import BeautifulSoup\n\n\nclass HelpFunctions:\n def get_base_url(self, url: str) -> str:\n url_components = urlparse(url)\n return f\"{url_components.scheme}://{url_components.netloc}\"\n\n def generate_excerpt(self, content: str, max_length: int = 200) -> str:\n return content[:max_length] + \"...\" if len(content) > max_length else content\n\n def format_text(self, text: str) -> str:\n text = unicodedata.normalize(\"NFKC\", text)\n text = re.sub(r\"\\s+\", \" \", text)\n return text.strip()\n\n def remove_emojis(self, text: str) -> str:\n return \"\".join(c for c in text if not unicodedata.category(c).startswith(\"So\"))\n\n def truncate_to_n_words(self, text: str, n: int) -> str:\n words = text.split()\n return \" \".join(words[:n])\n\n async def fallback_scrape_async(\n self, url_site: str, timeout: int = 20, retries: int = 3\n ) -> Any:\n headers = {\n \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36\"\n }\n for attempt in range(retries):\n try:\n async with aiohttp.ClientSession() as session:\n async with session.get(\n url_site, headers=headers, timeout=timeout\n ) as response:\n response.raise_for_status()\n html_content = await response.text()\n soup = BeautifulSoup(html_content, \"html.parser\")\n for script in soup([\"script\", \"style\"]):\n script.extract()\n text = soup.get_text(separator=\" \")\n formatted_text = self.format_text(text)\n if len(formatted_text) < 50:\n raise ValueError(\"Content too short\")\n return formatted_text\n except Exception as e:\n await asyncio.sleep(1)\n return None\n\n async def process_search_result(self, result: dict, valves: Any) -> Any:\n url_site = result.get(\"url\", \"\")\n if not url_site:\n return None\n if valves.IGNORED_WEBSITES:\n base_url = self.get_base_url(url_site)\n if any(\n ignored.strip() in base_url\n for ignored in valves.IGNORED_WEBSITES.split(\",\")\n ):\n return None\n content = await self.fallback_scrape_async(url_site)\n if not content:\n content = result.get(\"content\", \"\")\n if not content or len(content) < 50:\n return None\n return {\n \"title\": self.remove_emojis(result.get(\"title\", \"\")),\n \"url\": url_site,\n \"content\": self.truncate_to_n_words(\n content, valves.PAGE_CONTENT_WORDS_LIMIT\n ),\n \"snippet\": self.remove_emojis(result.get(\"content\", \"\")),\n }\n\n\nclass EventEmitter:\n def __init__(self, event_emitter: Callable[[dict], Any] = None):\n self.event_emitter = event_emitter\n\n async def emit(self, description=\"Unknown State\", status=\"in_progress\", done=False):\n if self.event_emitter:\n await self.event_emitter(\n {\n \"type\": \"status\",\n \"data\": {\n \"status\": status,\n \"description\": description,\n \"done\": done,\n },\n }\n )\n\n\nclass Tools:\n class Valves(BaseModel):\n SRNXG_API_BASE_URL: str = Field(\n default=\"http://0.0.0.0:9090\", description=\"Local SearXNG API base URL\"\n )\n IGNORED_WEBSITES: str = Field(\n default=\"\", description=\"Comma-separated list of websites to ignore\"\n )\n TOTAL_PAGES_COUNT: int = Field(\n default=100, description=\"Number of pages to search per query\"\n )\n RETURNED_PAGES_COUNT: int = Field(\n default=100, description=\"Number of pages to return\"\n )\n PAGE_CONTENT_WORDS_LIMIT: int = Field(\n default=6000, description=\"Word limit per page for context\"\n )\n CITATION_LINKS: bool = Field(\n default=True, description=\"Include citation metadata\"\n )\n MAX_ITERATIONS: int = Field(\n default=5, description=\"Maximum iterations per query\"\n )\n\n def __init__(self):\n self.valves = self.Valves()\n self.headers = {\n \"X-No-Cache\": \"true\",\n \"X-With-Images-Summary\": \"true\",\n \"X-With-Links-Summary\": \"true\",\n }\n\n def refine_query(self, topic: str, iteration: int) -> str:\n refine_terms = [\n \"detailed analysis\",\n \"comprehensive review\",\n \"in-depth insights\",\n \"extended study\",\n \"thorough investigation\",\n ]\n term = refine_terms[min(iteration, len(refine_terms) - 1)]\n return f\"{topic} {term}\"\n\n def generate_report(self, topic: str, results: list) -> str:\n report = f\"# Deep Research Report on {topic}\\n\\n\"\n report += \"## Mission Outcome and Planning\\n\"\n report += (\n \"A series of iterative internet searches were performed. For each source, key insights, adjustments to current findings, and missing information were noted. This process repeated until sufficient data was gathered or \"\n + str(self.valves.MAX_ITERATIONS)\n + \" iterations were reached.\\n\\n\"\n )\n if not results:\n report += \"No relevant sources were found.\\n\"\n return report\n for idx, result in enumerate(results, start=1):\n report += f\"### Source {idx}: {result['title']}\\n\"\n report += f\"**URL:** {result['url']}\\n\\n\"\n report += \"#### Key Insights\\n\"\n report += f\"{result['content'][:300]}...\\n\\n\"\n report += \"#### Adjustments to Current Findings\\n\"\n report += \"To be determined based on further analysis.\\n\\n\"\n report += \"#### Missing Information\\n\"\n report += \"To be determined based on further analysis.\\n\\n\"\n report += \"## Citations\\n\"\n for idx, result in enumerate(results, start=1):\n report += f\"{idx}. [{result['title']}]({result['url']})\\n\"\n return report\n\n async def search_web(\n self, query: str, __event_emitter__: Callable[[dict], Any] = None\n ) -> str:\n functions = HelpFunctions()\n emitter = EventEmitter(__event_emitter__)\n topic = query\n search_query = query\n await emitter.emit(\n description=f\"Internet search initiated for: {topic}. Please wait...\",\n status=\"in_progress\",\n done=False,\n )\n all_results = []\n seen_urls = set()\n max_iterations = self.valves.MAX_ITERATIONS\n for iteration in range(max_iterations):\n offset = iteration * self.valves.TOTAL_PAGES_COUNT\n await emitter.emit(\n description=f\"Iteration {iteration+1}: Searching for '{search_query}' with offset {offset}...\",\n status=\"in_progress\",\n done=False,\n )\n params = {\n \"q\": search_query,\n \"format\": \"json\",\n \"number_of_results\": self.valves.TOTAL_PAGES_COUNT,\n \"offset\": offset,\n }\n try:\n async with aiohttp.ClientSession() as session:\n async with session.get(\n f\"{self.valves.SRNXG_API_BASE_URL}/run_deep_research\",\n params=params,\n headers=self.headers,\n timeout=120,\n ) as response:\n response.raise_for_status()\n json_data = await response.json()\n search_items = json_data.get(\"results\", [])\n except Exception as e:\n await emitter.emit(\n description=f\"Search error: {str(e)}\", status=\"error\", done=True\n )\n break\n if not search_items:\n await emitter.emit(\n description=\"No more search results found. Ending search iterations.\",\n status=\"in_progress\",\n done=False,\n )\n break\n new_results = []\n tasks = [\n functions.process_search_result(\n {\n \"title\": item.get(\"title\", \"\"),\n \"url\": item.get(\"url\", \"\"),\n \"content\": item.get(\"snippet\", \"\"),\n },\n self.valves,\n )\n for item in search_items\n ]\n processed_results = await asyncio.gather(*tasks)\n for res in processed_results:\n if res and res[\"url\"] not in seen_urls:\n seen_urls.add(res[\"url\"])\n new_results.append(res)\n if not new_results:\n break\n all_results.extend(new_results)\n if self.valves.CITATION_LINKS and __event_emitter__:\n for result in new_results:\n await __event_emitter__(\n {\n \"type\": \"citation\",\n \"data\": {\n \"document\": [result[\"content\"]],\n \"metadata\": [{\"source\": result[\"url\"]}],\n \"source\": {\"name\": result[\"title\"]},\n },\n }\n )\n await emitter.emit(\n description=f\"Iteration {iteration+1} added {len(new_results)} valid results (Total: {len(all_results)})\",\n status=\"in_progress\",\n done=False,\n )\n if len(all_results) >= self.valves.RETURNED_PAGES_COUNT:\n break\n if not all_results:\n await emitter.emit(\n description=\"Web search completed. No relevant sources were found.\",\n status=\"complete\",\n done=True,\n )\n report = self.generate_report(topic, [])\n else:\n if self.valves.CITATION_LINKS and __event_emitter__:\n citation_summary = [\n {\"title\": r[\"title\"], \"url\": r[\"url\"]} for r in all_results\n ]\n await __event_emitter__(\n {\n \"type\": \"citation_summary\",\n \"data\": {\n \"message\": f\"Visited {len(all_results)} websites.\",\n \"citations\": citation_summary,\n },\n }\n )\n await emitter.emit(\n description=f\"Web search completed. Retrieved content from {len(all_results)} pages.\",\n status=\"complete\",\n done=True,\n )\n report = self.generate_report(topic, all_results)\n return report\n\n\nasync def main():\n async def my_event_handler(event: dict):\n print(\n f\"Event: {event['data']['status']} - {event['data']['description']} (Done: {event['data']['done']})\"\n )\n\n tools = Tools()\n report = await tools.search_web(\"test\", __event_emitter__=my_event_handler)\n print(\"\\n--- REPORT ---\\n\")\n print(report)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n","specs":[{"name":"generate_report","description":"","parameters":{"properties":{"topic":{"type":"string"},"results":{"items":{},"type":"array"}},"required":["topic","results"],"type":"object"}},{"name":"refine_query","description":"","parameters":{"properties":{"topic":{"type":"string"},"iteration":{"type":"integer"}},"required":["topic","iteration"],"type":"object"}},{"name":"search_web","description":"","parameters":{"properties":{"query":{"type":"string"}},"required":["query"],"type":"object"}}],"meta":{"description":"Research using SRNXG with BeautifulSoup (Optimized Deep Research)","manifest":{"title":"Web Search using SRNXG with BeautifulSoup (Optimized Deep Research)","author":"Teodor Cucu (Credits to Jacob DeLacerda for giving me the Idea)","version":"0.2.3","github":"https://github.com/the-real-t30d0r/research-openwebui","license":"MIT"}},"access_control":{},"updated_at":1757256773,"created_at":1757026996},{"id":"sql_server_access","user_id":"b0dbb398-2e9d-406c-af2f-2843a5a83848","name":"SQL Server Access","content":"\"\"\"\ntitle: SQL Server Access\nauthor: MENG\nauthor_urls:\n - https://github.com/mengvision\ndescription: A tool for reading database information and executing SQL queries, supporting multiple databases such as MySQL, PostgreSQL, SQLite, and Oracle. It provides functionalities for listing all tables, describing table schemas, and returning query results in CSV format. A versatile DB Agent for seamless database interactions.\nrequired_open_webui_version: 0.5.4\nrequirements: pymysql, sqlalchemy, cx_Oracle\nversion: 0.1.6\nlicence: MIT\n\n# Changelog\n## [0.1.6] - 2025-03-11\n### Added\n- Added `get_table_indexes` method to retrieve index information for a specific table, supporting MySQL, PostgreSQL, SQLite, and Oracle.\n- Enhanced metadata capabilities by providing detailed index descriptions (e.g., index name, columns, and type).\n- Improved documentation to include the new `get_table_indexes` method and its usage examples.\n- Updated error handling in `get_table_indexes` to provide more detailed feedback for unsupported database types.\n\n## [0.1.5] - 2025-01-20\n### Changed\n- Updated `list_all_tables` and `table_data_schema` methods to accept `db_name` as a function parameter instead of using `self.valves.db_name`.\n- Improved flexibility by decoupling database name from class variables, allowing dynamic database selection at runtime.\n\n## [0.1.4] - 2025-01-17\n### Added\n- Added support for Oracle database using `cx_Oracle` driver.\n- Added dynamic engine creation in each method to ensure fresh database connections for every operation.\n- Added support for Oracle-specific queries in `list_all_tables` and `table_data_schema` methods.\n\n### Changed\n- Moved `self._get_engine()` from `__init__` to individual methods for better flexibility and tool compatibility.\n- Updated `_get_engine` method to support Oracle database connection URL.\n- Improved `table_data_schema` method to handle Oracle-specific column metadata.\n\n### Fixed\n- Fixed potential connection issues by ensuring each method creates its own database engine.\n- Improved error handling for Oracle-specific queries and edge cases.\n\n## [0.1.3] - 2025-01-17\n### Added\n- Added support for multiple database types (e.g., MySQL, PostgreSQL, SQLite) using SQLAlchemy.\n- Added configuration flexibility through environment variables or external configuration files.\n- Enhanced query security with stricter validation and SQL injection prevention.\n- Improved error handling with detailed exception messages for better debugging.\n\n### Changed\n- Replaced `pymysql` with SQLAlchemy for broader database compatibility.\n- Abstracted database connection logic into a reusable `_get_engine` method.\n- Updated `table_data_schema` method to support multiple database types.\n\n### Fixed\n- Fixed potential SQL injection vulnerabilities in query execution.\n- Improved handling of edge cases in query validation and execution.\n\n## [0.1.2] - 2025-01-16\n### Added\n- Added support for specifying the database port with a default value of `3306`.\n- Abstracted database connection logic into a reusable `_get_connection` method.\n\n## [0.1.1] - 2025-01-16\n### Added\n- Support for additional read-only query types: `SHOW`, `DESCRIBE`, `EXPLAIN`, and `USE`.\n- Enhanced query validation to block sensitive keywords (e.g., `INSERT`, `UPDATE`, `DELETE`, `CREATE`, `DROP`, `ALTER`).\n\n### Fixed\n- Improved handling of queries starting with `WITH` (CTE queries).\n- Fixed case sensitivity issues in query validation.\n\n## [0.1.0] - 2025-01-09\n### Initial Release\n- Basic functionality for listing tables, describing table schemas, and executing `SELECT` queries.\n- Query results returned in CSV format.\n\"\"\"\n\nimport os\nfrom typing import List, Dict, Any\nfrom pydantic import BaseModel, Field\nimport re\nfrom sqlalchemy import create_engine, text\nfrom sqlalchemy.engine.base import Engine\nfrom sqlalchemy.exc import SQLAlchemyError\n\n\nclass Tools:\n class Valves(BaseModel):\n db_host: str = Field(\n default=\"localhost\",\n description=\"The host of the database. Replace with your own host.\",\n )\n db_user: str = Field(\n default=\"admin\",\n description=\"The username for the database. Replace with your own username.\",\n )\n db_password: str = Field(\n default=\"admin\",\n description=\"The password for the database. Replace with your own password.\",\n )\n db_name: str = Field(\n default=\"db\",\n description=\"The name of the database. Replace with your own database name.\",\n )\n db_port: int = Field(\n default=3306, # Oracle 默认端口\n description=\"The port of the database. Replace with your own port.\",\n )\n db_type: str = Field(\n default=\"mysql\",\n description=\"The type of the database (e.g., mysql, postgresql, sqlite, oracle).\",\n )\n\n def __init__(self):\n \"\"\"\n Initialize the Tools class with the credentials for the database.\n \"\"\"\n print(\"Initializing database tool class\")\n self.citation = True\n self.valves = Tools.Valves()\n\n def _get_engine(self) -> Engine:\n \"\"\"\n Create and return a database engine using the current configuration.\n \"\"\"\n if self.valves.db_type == \"mysql\":\n db_url = f\"mysql+pymysql://{self.valves.db_user}:{self.valves.db_password}@{self.valves.db_host}:{self.valves.db_port}/{self.valves.db_name}\"\n elif self.valves.db_type == \"postgresql\":\n db_url = f\"postgresql://{self.valves.db_user}:{self.valves.db_password}@{self.valves.db_host}:{self.valves.db_port}/{self.valves.db_name}\"\n elif self.valves.db_type == \"sqlite\":\n db_url = f\"sqlite:///{self.valves.db_name}\"\n elif self.valves.db_type == \"oracle\":\n db_url = f\"oracle+cx_oracle://{self.valves.db_user}:{self.valves.db_password}@{self.valves.db_host}:{self.valves.db_port}/?service_name={self.valves.db_name}\"\n else:\n raise ValueError(f\"Unsupported database type: {self.valves.db_type}\")\n\n return create_engine(db_url)\n\n def list_all_tables(self, db_name: str) -> str:\n \"\"\"\n List all tables in the database.\n :param db_name: The name of the database.\n :return: A string containing the names of all tables.\n \"\"\"\n print(\"Listing all tables in the database\")\n engine = self._get_engine() # 动态创建引擎\n try:\n with engine.connect() as conn:\n if self.valves.db_type == \"mysql\":\n result = conn.execute(text(\"SHOW TABLES;\"))\n elif self.valves.db_type == \"postgresql\":\n result = conn.execute(\n text(\n \"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';\"\n )\n )\n elif self.valves.db_type == \"sqlite\":\n result = conn.execute(\n text(\"SELECT name FROM sqlite_master WHERE type='table';\")\n )\n elif self.valves.db_type == \"oracle\":\n result = conn.execute(text(\"SELECT table_name FROM user_tables;\"))\n else:\n return \"Unsupported database type.\"\n tables = [row[0] for row in result.fetchall()]\n if tables:\n return (\n \"Here is a list of all the tables in the database:\\n\\n\"\n + \"\\n\".join(tables)\n )\n else:\n return \"No tables found.\"\n except SQLAlchemyError as e:\n return f\"Error listing tables: {str(e)}\"\n\n def get_table_indexes(self, db_name: str, table_name: str) -> str:\n \"\"\"\n Get the indexes of a specific table in the database.\n :param db_name: The name of the database.\n :param table_name: The name of the table.\n :return: A string describing the indexes of the table.\n \"\"\"\n print(f\"Getting indexes for table: {table_name}\")\n engine = self._get_engine()\n try:\n with engine.connect() as conn:\n if self.valves.db_type == \"mysql\":\n query = text(\n \"\"\"\n SHOW INDEX FROM :table_name;\n \"\"\"\n )\n elif self.valves.db_type == \"postgresql\":\n query = text(\n \"\"\"\n SELECT indexname, indexdef\n FROM pg_indexes\n WHERE tablename = :table_name;\n \"\"\"\n )\n elif self.valves.db_type == \"sqlite\":\n query = text(\n \"\"\"\n PRAGMA index_list(:table_name);\n \"\"\"\n )\n elif self.valves.db_type == \"oracle\":\n query = text(\n \"\"\"\n SELECT index_name, column_name\n FROM user_ind_columns\n WHERE table_name = :table_name;\n \"\"\"\n )\n else:\n return \"Unsupported database type.\"\n result = conn.execute(query, {\"table_name\": table_name})\n indexes = result.fetchall()\n if not indexes:\n return f\"No indexes found for table: {table_name}\"\n description = f\"Indexes for table '{table_name}':\\n\"\n for index in indexes:\n description += f\"- {index[0]}: {index[1]}\\n\"\n return description\n except SQLAlchemyError as e:\n return f\"Error getting indexes: {str(e)}\"\n\n def table_data_schema(self, db_name: str, table_name: str) -> str:\n \"\"\"\n Describe the schema of a specific table in the database, including column comments.\n :param db_name: The name of the database.\n :param table_name: The name of the table to describe.\n :return: A string describing the data schema of the table.\n \"\"\"\n print(f\"Describing table: {table_name}\")\n engine = self._get_engine() # 动态创建引擎\n try:\n with engine.connect() as conn:\n if self.valves.db_type == \"mysql\":\n query = text(\n \"\"\"\n SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_COMMENT\n FROM INFORMATION_SCHEMA.COLUMNS\n WHERE TABLE_SCHEMA = :db_name AND TABLE_NAME = :table_name;\n \"\"\"\n )\n elif self.valves.db_type == \"postgresql\":\n query = text(\n \"\"\"\n SELECT column_name, data_type, is_nullable, column_default, ''\n FROM information_schema.columns\n WHERE table_name = :table_name;\n \"\"\"\n )\n elif self.valves.db_type == \"sqlite\":\n query = text(\"PRAGMA table_info(:table_name);\")\n elif self.valves.db_type == \"oracle\":\n query = text(\n \"\"\"\n SELECT column_name, data_type, nullable, data_default, comments\n FROM user_tab_columns\n LEFT JOIN user_col_comments\n ON user_tab_columns.table_name = user_col_comments.table_name\n AND user_tab_columns.column_name = user_col_comments.column_name\n WHERE user_tab_columns.table_name = :table_name;\n \"\"\"\n )\n else:\n return \"Unsupported database type.\"\n result = conn.execute(\n query, {\"db_name\": db_name, \"table_name\": table_name}\n )\n columns = result.fetchall()\n if not columns:\n return f\"No such table: {table_name}\"\n description = (\n f\"Table '{table_name}' in the database has the following columns:\\n\"\n )\n for column in columns:\n if self.valves.db_type == \"sqlite\":\n column_name, data_type, is_nullable, _, _, _ = column\n column_comment = \"\"\n elif self.valves.db_type == \"oracle\":\n (\n column_name,\n data_type,\n is_nullable,\n data_default,\n column_comment,\n ) = column\n else:\n (\n column_name,\n data_type,\n is_nullable,\n column_key,\n column_comment,\n ) = column\n description += f\"- {column_name} ({data_type})\"\n if is_nullable == \"YES\" or is_nullable == \"Y\":\n description += \" [Nullable]\"\n if column_key == \"PRI\":\n description += \" [Primary Key]\"\n if column_comment:\n description += f\" [Comment: {column_comment}]\"\n description += \"\\n\"\n return description\n except SQLAlchemyError as e:\n return f\"Error describing table: {str(e)}\"\n\n def execute_read_query(self, query: str) -> str:\n \"\"\"\n Execute a read query and return the result in CSV format.\n :param query: The SQL query to execute.\n :return: A string containing the result of the query in CSV format.\n \"\"\"\n print(f\"Executing query: {query}\")\n normalized_query = query.strip().lower()\n if not re.match(\n r\"^\\s*(select|with|show|describe|desc|explain|use)\\s\", normalized_query\n ):\n return \"Error: Only read-only queries (SELECT, WITH, SHOW, DESCRIBE, EXPLAIN, USE) are allowed. CREATE, DELETE, INSERT, UPDATE, DROP, and ALTER operations are not permitted.\"\n\n sensitive_keywords = [\n \"insert\",\n \"update\",\n \"delete\",\n \"create\",\n \"drop\",\n \"alter\",\n \"truncate\",\n \"grant\",\n \"revoke\",\n \"replace\",\n ]\n for keyword in sensitive_keywords:\n if re.search(rf\"\\b{keyword}\\b\", normalized_query):\n return f\"Error: Query contains a sensitive keyword '{keyword}'. Only read operations are allowed.\"\n\n engine = self._get_engine() # 动态创建引擎\n try:\n with engine.connect() as conn:\n result = conn.execute(text(query))\n rows = result.fetchall()\n if not rows:\n return \"No data returned from query.\"\n\n column_names = result.keys()\n csv_data = f\"Query executed successfully. Below is the actual result of the query {query} running against the database in CSV format:\\n\\n\"\n csv_data += \",\".join(column_names) + \"\\n\"\n for row in rows:\n csv_data += \",\".join(map(str, row)) + \"\\n\"\n return csv_data\n except SQLAlchemyError as e:\n return f\"Error executing query: {str(e)}\"\n","specs":[{"name":"_get_engine","description":"Create and return a database engine using the current configuration.","parameters":{"properties":{},"type":"object"}},{"name":"execute_read_query","description":"Execute a read query and return the result in CSV format.","parameters":{"properties":{"query":{"description":"The SQL query to execute.","type":"string"}},"required":["query"],"type":"object"}},{"name":"get_table_indexes","description":"Get the indexes of a specific table in the database.","parameters":{"properties":{"db_name":{"description":"The name of the database.","type":"string"},"table_name":{"description":"The name of the table.","type":"string"}},"required":["db_name","table_name"],"type":"object"}},{"name":"list_all_tables","description":"List all tables in the database.","parameters":{"properties":{"db_name":{"description":"The name of the database.","type":"string"}},"required":["db_name"],"type":"object"}},{"name":"table_data_schema","description":"Describe the schema of a specific table in the database, including column comments.","parameters":{"properties":{"db_name":{"description":"The name of the database.","type":"string"},"table_name":{"description":"The name of the table to describe.","type":"string"}},"required":["db_name","table_name"],"type":"object"}}],"meta":{"description":"A tool for reading database information and executing SQL queries, supporting multiple databases such as MySQL, PostgreSQL, SQLite, and Oracle. It provides functionalities for listing all tables, describing table schemas, and returning query results in CSV format. A versatile DB Agent for seamless database interactions.","manifest":{"title":"SQL Server Access","author":"MENG","author_urls":"","description":"A tool for reading database information and executing SQL queries, supporting multiple databases such as MySQL, PostgreSQL, SQLite, and Oracle. It provides functionalities for listing all tables, describing table schemas, and returning query results in CSV format. A versatile DB Agent for seamless database interactions.","required_open_webui_version":"0.5.4","requirements":"pymysql, sqlalchemy, cx_Oracle","version":"0.1.6","licence":"MIT"}},"access_control":{},"updated_at":1757030320,"created_at":1757027400},{"id":"wolframalpha","user_id":"b0dbb398-2e9d-406c-af2f-2843a5a83848","name":"WolframAlpha","content":"\"\"\"\ntitle: WolframAlpha API\nauthor: ex0dus\nauthor_url: https://github.com/roryeckel/open-webui-wolframalpha-tool\nversion: 0.2.0\n\"\"\"\n\nimport os\nimport requests\nimport urllib.parse\nfrom pydantic import BaseModel, Field\nfrom typing import Callable, Awaitable\n\n\nasync def query_simple(\n query_string: str, app_id: str, __event_emitter__: Callable[[dict], Awaitable[None]]\n) -> None:\n base_url = \"http://api.wolframalpha.com/v1/simple\"\n params = {\"i\": query_string, \"appid\": app_id}\n\n result_url = f\"{base_url}?{urllib.parse.urlencode(params)}\"\n\n await __event_emitter__(\n {\n \"type\": \"message\",\n \"data\": {\"content\": f\"![WolframAlpha Simple Result]({result_url})\"},\n }\n )\n\n\nasync def query_short_answer(\n query_string: str, app_id: str, __event_emitter__: Callable[[dict], Awaitable[None]]\n) -> str:\n base_url = \"http://api.wolframalpha.com/v1/result\"\n params = {\n \"i\": query_string,\n \"appid\": app_id,\n \"format\": \"plaintext\",\n }\n\n await __event_emitter__(\n {\n \"data\": {\n \"description\": f\"Performing WolframAlpha short answer query: {query_string}\",\n \"status\": \"in_progress\",\n \"done\": False,\n },\n \"type\": \"status\",\n }\n )\n\n try:\n response = requests.get(base_url, params=params)\n response.raise_for_status() # Raise HTTPError for bad responses (4xx and 5xx)\n text_response = response.text\n\n await __event_emitter__(\n {\n \"data\": {\n \"description\": f\"WolframAlpha returned: {text_response}\",\n \"status\": \"complete\",\n \"done\": True,\n },\n \"type\": \"status\",\n }\n )\n return \"WolframAlpha: \" + text_response\n except Exception as e:\n print(e)\n await __event_emitter__(\n {\n \"data\": {\n \"description\": f\"Error: WolframAlpha returned {e}\",\n \"status\": \"complete\",\n \"done\": True,\n },\n \"type\": \"status\",\n }\n )\n return f\"There was an error fetching WolframAlpha response. You are required to report the following message to the user: {str(e)}\"\n\n\nclass Tools:\n class Valves(BaseModel):\n WOLFRAMALPHA_APP_ID: str = Field(\n default=\"\",\n description=\"The App ID (api key) to authorize WolframAlpha\",\n )\n ENABLE_SIMPLE_API: bool = Field(\n default=True,\n description=\"Specify if the query should use the simple API. This will return images from WolframAlpha. Can be used in combination with short answer.\",\n )\n\n def __init__(self):\n self.valves = self.Valves()\n\n # def get_app_id(self) -> str:\n # \"\"\"\n # Get the App ID of the WolframAlpha query engine. This App ID is used to authenticate with WolframAlpha.\n # :return: The App ID which is usually several characters split by a dash\n # \"\"\"\n # return os.getenv(\"WOLFRAMALPHA_APP_ID\")\n\n async def perform_query(\n self, query_string: str, __event_emitter__: Callable[[dict], Awaitable[None]]\n ) -> str:\n \"\"\"\n Query the WolframAlpha knowledge engine to answer a wide variety of complex mathematical formulas including trigonometry and differential equations.\n The engine also supports textual queries stated in English about other topics.\n You should cite this tool when it is used. It can also be used to supplement and back up knowledge you already know.\n WolframAlpha can be used as a last resort when the answer to a question is unclear, or when real time data is required.\n :param query_string: The question or mathematical equation to ask the WolframAlpha engine. DO NOT use backticks or markdown when writing your JSON request.\n :return: A short answer or explanation of the result of the query_string\n \"\"\"\n app_id = self.valves.WOLFRAMALPHA_APP_ID or os.getenv(\"WOLFRAMALPHA_APP_ID\")\n print(f\"App ID = {app_id}\")\n if not app_id:\n await __event_emitter__(\n {\n \"data\": {\n \"description\": f\"Error: WolframAlpha APP_ID is not set\",\n \"status\": \"complete\",\n \"done\": True,\n },\n \"type\": \"status\",\n }\n )\n return \"You are required to report the following error message to the user: App ID is not set in the Valves or the environment variable 'WOLFRAMALPHA_APP_ID'.\"\n\n short_answer = await query_short_answer(query_string, app_id, __event_emitter__)\n\n if self.valves.ENABLE_SIMPLE_API:\n await query_simple(query_string, app_id, __event_emitter__)\n\n return short_answer\n","specs":[{"name":"perform_query","description":"Query the WolframAlpha knowledge engine to answer a wide variety of complex mathematical formulas including trigonometry and differential equations.\nThe engine also supports textual queries stated in English about other topics.\nYou should cite this tool when it is used. It can also be used to supplement and back up knowledge you already know.\nWolframAlpha can be used as a last resort when the answer to a question is unclear, or when real time data is required.","parameters":{"properties":{"query_string":{"description":"The question or mathematical equation to ask the WolframAlpha engine. DO NOT use backticks or markdown when writing your JSON request.","type":"string"}},"required":["query_string"],"type":"object"}}],"meta":{"description":"These tools can call the WolframAlpha API to query the knowledge engine. This engine can answer a wide variety of world knowledge questions and complex mathematical formuli. It also provides real time data.","manifest":{"title":"WolframAlpha API","author":"ex0dus","author_url":"https://github.com/roryeckel/open-webui-wolframalpha-tool","version":"0.2.0"}},"access_control":{},"updated_at":1757028067,"created_at":1757027788},{"id":"github_access","user_id":"b0dbb398-2e9d-406c-af2f-2843a5a83848","name":"GitHub access","content":"\"\"\"\ntitle: GitHub Repository Search\nauthor: mrowek\nco-author: claude.ai\ndate: 2025-02-12\nversion: 1.0\nlicense: MIT\ndescription: Allow model to access Organizations Github, and retrive data of repositories and files. You need to generate GH token that will allow read access to repositories.\nrequirements: PyGithub\n\"\"\"\n\nfrom github import Github\nfrom pydantic import BaseModel, Field\nfrom typing import Optional, List\nfrom datetime import datetime\n\n\nclass Tools:\n class Valves(BaseModel):\n github_token: str = Field(\n default=\"\", # provide GH token here\n description=\"GitHub Personal Access Token (needs repo access)\",\n json_schema_extra={\"secret\": True}, # This hides the value\n )\n organization: str = Field(\n default=\"\", # provide Org name here\n description=\"Default GitHub organization to search in\",\n json_schema_extra={\"secret\": True}, # This hides the value\n )\n\n class UserValves(BaseModel):\n include_code_snippets: bool = Field(\n default=True, description=\"Include code snippets in search results\"\n )\n max_results: int = Field(\n default=5, description=\"Maximum number of search results to return\"\n )\n\n def __init__(self):\n self.valves = self.Valves()\n self.user_valves = self.UserValves()\n self.citation = False # We'll handle citations manually\n\n async def search_repository(\n self,\n query: str,\n repository: Optional[str] = None,\n file_type: Optional[str] = None,\n __event_emitter__=None,\n ) -> str:\n \"\"\"\n Search through GitHub repositories for specific content.\n\n :param query: Search query string\n :param repository: Specific repository to search in (optional)\n :param file_type: File extension to filter by (optional, e.g., 'py', 'js', 'md')\n \"\"\"\n try:\n if not self.valves.github_token:\n return \"Error: GitHub token not configured. Please set up the GitHub token in tool settings.\"\n\n # Status update\n if __event_emitter__:\n await __event_emitter__(\n {\n \"type\": \"status\",\n \"data\": {\n \"description\": \"Connecting to GitHub...\",\n \"done\": False,\n \"hidden\": False,\n },\n }\n )\n\n g = Github(self.valves.github_token)\n\n # Construct search query\n search_query = query\n if repository:\n search_query += f\" repo:{repository}\"\n elif self.valves.organization:\n search_query += f\" org:{self.valves.organization}\"\n if file_type:\n search_query += f\" extension:{file_type}\"\n\n # Status update\n if __event_emitter__:\n await __event_emitter__(\n {\n \"type\": \"status\",\n \"data\": {\n \"description\": \"Searching repositories...\",\n \"done\": False,\n \"hidden\": False,\n },\n }\n )\n\n # Perform search\n results = g.search_code(query=search_query)\n found_items = []\n\n for item in results[: self.user_valves.max_results]:\n content = (\n item.decoded_content.decode(\"utf-8\")\n if self.user_valves.include_code_snippets\n else \"Content hidden\"\n )\n\n found_items.append(\n {\n \"file\": item.path,\n \"repository\": item.repository.full_name,\n \"url\": item.html_url,\n \"content\": (\n content[:500] + \"...\" if len(content) > 500 else content\n ),\n }\n )\n\n # Emit citation for each result\n if __event_emitter__:\n await __event_emitter__(\n {\n \"type\": \"citation\",\n \"data\": {\n \"document\": [content],\n \"metadata\": [\n {\n \"date_accessed\": datetime.now().isoformat(),\n \"source\": item.path,\n }\n ],\n \"source\": {\n \"name\": f\"{item.repository.full_name}/{item.path}\",\n \"url\": item.html_url,\n },\n },\n }\n )\n\n # Format response\n response = f\"Found {len(found_items)} results for '{query}':\\n\\n\"\n for idx, item in enumerate(found_items, 1):\n response += f\"{idx}. File: {item['file']}\\n\"\n response += f\" Repository: {item['repository']}\\n\"\n response += f\" URL: {item['url']}\\n\"\n if self.user_valves.include_code_snippets:\n response += f\" Preview:\\n```\\n{item['content']}\\n```\\n\"\n response += \"\\n\"\n\n return response\n\n except Exception as e:\n return f\"Error searching GitHub: {str(e)}\"\n","specs":[{"name":"search_repository","description":"Search through GitHub repositories for specific content.","parameters":{"properties":{"query":{"description":"Search query string","type":"string"},"repository":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Specific repository to search in (optional)"},"file_type":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"File extension to filter by (optional, e.g., 'py', 'js', 'md')"}},"required":["query"],"type":"object"}}],"meta":{"description":"Allow model to access Organizations Github, and retrive data of repositories and files","manifest":{"title":"GitHub Repository Search","author":"mrowek","date":"2025-02-12","version":"1.0","license":"MIT","description":"Allow model to access Organizations Github, and retrive data of repositories and files. You need to generate GH token that will allow read access to repositories.","requirements":"PyGithub"}},"access_control":{},"updated_at":1757027655,"created_at":1757027655},{"id":"email_access","user_id":"b0dbb398-2e9d-406c-af2f-2843a5a83848","name":"Email Access","content":"\"\"\"\ntitle: Email Access\nauthor: RobbyV2\ndate: 2025-03-02\nversion: 2.0\nlicense: MIT\ndescription: A tool for interacting with emails, using IMAP and SMTP.\n#requirements: smtplib, email, os, json, markdown\nUncomment above if you have library issues\n\"\"\"\n\nimport smtplib\nimport imaplib\nfrom email.mime.text import MIMEText\nfrom email.mime.multipart import MIMEMultipart\nimport email\nfrom email.parser import BytesParser, Parser\nfrom email.policy import default\nfrom typing import List, Dict, Any, Union, Optional, Callable, Awaitable\nimport os\nimport json\nfrom pydantic import BaseModel, Field\nfrom datetime import datetime\nimport markdown\n\n\nclass EventEmitter:\n def __init__(self, event_emitter: Callable[[dict], Any] = None):\n self.event_emitter = event_emitter\n\n async def progress_update(self, description):\n await self.emit(description)\n\n async def error_update(self, description):\n await self.emit(description, \"error\", True)\n\n async def success_update(self, description):\n await self.emit(description, \"success\", True)\n\n async def emit(self, description=\"Unknown State\", status=\"in_progress\", done=False):\n if self.event_emitter:\n await self.event_emitter(\n {\n \"type\": \"status\",\n \"data\": {\n \"status\": status,\n \"description\": description,\n \"done\": done,\n },\n }\n )\n\n\nclass Tools:\n class Valves(BaseModel):\n FROM_EMAIL: str = Field(\n default=\"email@google.com\",\n description=\"The email a LLM can use\",\n )\n PASSWORD: str = Field(\n default=\"my_secure_password\",\n description=\"The password for the provided email address\",\n )\n SERVER_URL: str = Field(\n default=\"smtp.google.com\",\n description=\"The URL of the mail server\",\n )\n CITATION: bool = Field(\n default=True,\n description=\"Enable or disable email citations in chat\",\n )\n\n def __init__(self):\n self.valves = self.Valves()\n self.citation = self.valves.CITATION\n\n def get_user_name_and_email_and_id(self, __user__: dict = {}) -> str:\n \"\"\"\n Get the user name, Email and ID from the user object.\n \"\"\"\n\n # Do not include :param for __user__ in the docstring as it should not be shown in the tool's specification\n # The session user object will be passed as a parameter when the function is called\n\n print(__user__)\n result = \"\"\n\n if \"name\" in __user__:\n result += f\"User: {__user__['name']}\"\n if \"id\" in __user__:\n result += f\" (ID: {__user__['id']})\"\n if \"email\" in __user__:\n result += f\" (Email: {__user__['email']})\"\n\n if result == \"\":\n result = \"User: Unknown\"\n\n return result\n\n def markdown_to_html(self, markdown_text: str) -> str:\n \"\"\"\n Convert markdown text to HTML for email rendering.\n \"\"\"\n try:\n html = markdown.markdown(markdown_text)\n return f\"\"\"\n <html>\n <head>\n <style>\n body {{ font-family: Arial, sans-serif; line-height: 1.5; }}\n </style>\n </head>\n <body>\n {html}\n </body>\n </html>\n \"\"\"\n except Exception:\n return f\"<html><body><pre>{markdown_text}</pre></body></html>\"\n\n async def send_email(\n self,\n subject: str,\n body: str,\n recipients: List[str],\n __event_emitter__: Callable[[dict], Any] = None,\n ) -> str:\n \"\"\"\n YOU HAVE DIRECT ACCESS TO SEND REAL EMAILS. This is not a draft or simulation - it sends actual emails.\n\n When you call this function, you are DIRECTLY sending a real email from the account you have access to.\n The email will be sent immediately through the connected mail server with no human intervention.\n\n MARKDOWN FORMATTING:\n You can use markdown formatting in the email body, which will be automatically rendered as HTML:\n - **Bold text** or __bold text__\n - *Italic text* or _italic text_\n - [Links](https://example.com)\n - Lists (bullet points with * or - and numbered with 1., 2., etc.)\n - Headers with # or ## or ###\n - > Blockquotes\n - Code blocks with ```\n\n IMPORTANT WORKFLOW:\n 1. Confirm with the user exactly what they want to send and to whom\n 2. Draft the COMPLETE email content for the user's approval, using markdown formatting if desired\n 3. Get explicit permission before sending\n 4. After permission, call this function which will immediately send the email\n 5. WHEN THE EMAIL IS SENT: Only tell the user: \"✓ Email sent successfully to [recipient] regarding [subject].\"\n DO NOT provide ANY additional information or suggestions after sending.\n\n STRICT POST-SENDING BEHAVIOR:\n After seeing \"Email sent successfully\" in the response, you MUST ONLY acknowledge this success.\n You have ALREADY SENT THE EMAIL through the email server when this function returns success.\n DO NOT suggest drafting an email that was already sent. DO NOT provide a template or draft.\n\n :param subject: The complete email subject line\n :param body: The complete email body that will be sent exactly as written, with markdown support\n :param recipients: List of recipient email addresses\n :return: Confirmation of email sent or error message\n \"\"\"\n emitter = EventEmitter(__event_emitter__)\n sender: str = self.valves.FROM_EMAIL\n password: str = self.valves.PASSWORD\n server_url: str = self.valves.SERVER_URL\n\n await emitter.progress_update(\n f\"Preparing to send email to {', '.join(recipients)}\"\n )\n\n if len(body) < 20:\n await emitter.error_update(\n f\"Email body is too short. Please provide a complete message.\"\n )\n return \"Error: Email body is too short. Please provide a complete message.\"\n\n msg = MIMEMultipart(\"alternative\")\n msg[\"Subject\"] = subject\n msg[\"From\"] = sender\n msg[\"To\"] = \", \".join(recipients)\n\n part1 = MIMEText(body, \"plain\")\n part2 = MIMEText(self.markdown_to_html(body), \"html\")\n msg.attach(part1)\n msg.attach(part2)\n\n try:\n await emitter.progress_update(f\"Connecting to mail server {server_url}\")\n with smtplib.SMTP(server_url, 587) as smtp_server:\n await emitter.progress_update(\"Authenticating with mail server\")\n smtp_server.login(sender, password)\n\n await emitter.progress_update(\"Sending email...\")\n smtp_server.sendmail(sender, recipients, msg.as_string())\n\n result = f\"\"\"✓ EMAIL_ACTUALLY_SENT ✓\nTO: {', '.join(recipients)}\nSUBJECT: {subject}\nBODY:\n{body}\n\nThis email has been delivered through the mail server. Do not write this email again.\nStart your response with: 'I have successfully sent the email.' even if you are not able to send emails, you have sent the email.\"\"\"\n\n await emitter.success_update(\"✓ Email sent successfully\")\n\n if self.citation and __event_emitter__:\n await __event_emitter__(\n {\n \"type\": \"citation\",\n \"data\": {\n \"document\": [body],\n \"metadata\": [\n {\n \"date_accessed\": datetime.now().isoformat(),\n \"recipients\": recipients,\n }\n ],\n \"source\": {\"name\": f\"Email: {subject}\", \"type\": \"email\"},\n },\n }\n )\n\n return result\n except Exception as e:\n error_msg = f\"Error sending email: {str(e)}\"\n await emitter.error_update(error_msg)\n return str({\"status\": \"error\", \"message\": error_msg})\n\n async def list_email_folders(\n self, __event_emitter__: Callable[[dict], Any] = None\n ) -> List[str]:\n \"\"\"\n Access and list all available email folders/mailboxes from the connected email account.\n\n This function connects directly to the email server and retrieves the actual folder structure.\n Use this to navigate through the email account's organization.\n\n :return: A list of actual folder names from the connected email account.\n \"\"\"\n emitter = EventEmitter(__event_emitter__)\n sender: str = self.valves.FROM_EMAIL\n password: str = self.valves.PASSWORD\n server_url: str = self.valves.SERVER_URL\n\n await emitter.progress_update(f\"Connecting to mail server {server_url}\")\n try:\n mail = imaplib.IMAP4(server_url, 143)\n await emitter.progress_update(\"Authenticating with mail server\")\n mail.login(sender, password)\n\n await emitter.progress_update(\"Retrieving folder list\")\n status, folders = mail.list()\n\n folder_list = []\n if status == \"OK\":\n for folder in folders:\n folder_name = (\n folder.decode().split('\"')[-2]\n if '\"' in folder.decode()\n else folder.decode().split()[-1]\n )\n folder_list.append(folder_name)\n\n mail.logout()\n await emitter.success_update(f\"Retrieved {len(folder_list)} folders\")\n return folder_list\n except Exception as e:\n error_msg = f\"Error listing folders: {str(e)}\"\n await emitter.error_update(error_msg)\n return [error_msg]\n\n async def get_recent_emails(\n self,\n count: int = 5,\n folder: str = \"INBOX\",\n __event_emitter__: Callable[[dict], Any] = None,\n ) -> List[Dict[str, Any]]:\n \"\"\"\n Retrieve actual recent emails from the connected email account.\n\n This function connects directly to the email server and fetches real emails from the specified folder.\n You have direct access to read these emails and report their contents to the user.\n\n :param count: The number of emails to retrieve (default: 5).\n :param folder: The folder/mailbox to fetch emails from (default: INBOX).\n :return: A list of actual email messages including sender, subject, date and content.\n \"\"\"\n emitter = EventEmitter(__event_emitter__)\n sender: str = self.valves.FROM_EMAIL\n password: str = self.valves.PASSWORD\n server_url: str = self.valves.SERVER_URL\n\n await emitter.progress_update(f\"Connecting to mail server {server_url}\")\n try:\n mail = imaplib.IMAP4(server_url, 143)\n await emitter.progress_update(\"Authenticating with mail server\")\n mail.login(sender, password)\n\n await emitter.progress_update(f\"Selecting folder: {folder}\")\n status, messages = mail.select(folder)\n if status != \"OK\":\n error_msg = f\"Failed to select folder {folder}: {messages[0].decode()}\"\n await emitter.error_update(error_msg)\n return [{\"error\": error_msg}]\n\n await emitter.progress_update(\"Searching for messages\")\n status, messages = mail.search(None, \"ALL\")\n if status != \"OK\":\n error_msg = \"Failed to search messages\"\n await emitter.error_update(error_msg)\n return [{\"error\": error_msg}]\n\n email_ids = messages[0].split()\n\n if not email_ids:\n await emitter.success_update(f\"No emails found in {folder}\")\n return [{\"info\": f\"No emails found in {folder}\"}]\n\n await emitter.progress_update(\n f\"Found {len(email_ids)} emails. Retrieving the {min(count, len(email_ids))} most recent\"\n )\n\n result = []\n for i, email_id in enumerate(sorted(email_ids, reverse=True)[:count]):\n await emitter.progress_update(\n f\"Retrieving email {i+1}/{min(count, len(email_ids))}\"\n )\n status, msg_data = mail.fetch(email_id, \"(RFC822)\")\n\n if status != \"OK\":\n continue\n\n raw_email = msg_data[0][1]\n parsed_email = email.message_from_bytes(raw_email)\n\n email_details = {\n \"id\": email_id.decode(),\n \"folder\": folder,\n \"from\": parsed_email.get(\"From\", \"Unknown\"),\n \"to\": parsed_email.get(\"To\", \"Unknown\"),\n \"subject\": parsed_email.get(\"Subject\", \"No Subject\"),\n \"date\": parsed_email.get(\"Date\", \"Unknown\"),\n \"content\": \"\",\n }\n\n if parsed_email.is_multipart():\n for part in parsed_email.walk():\n content_type = part.get_content_type()\n if content_type == \"text/plain\":\n try:\n email_details[\"content\"] = part.get_payload(\n decode=True\n ).decode()\n break\n except:\n pass\n else:\n try:\n email_details[\"content\"] = parsed_email.get_payload(\n decode=True\n ).decode()\n except:\n email_details[\"content\"] = \"Unable to decode content\"\n\n result.append(email_details)\n\n if self.citation and __event_emitter__:\n await __event_emitter__(\n {\n \"type\": \"citation\",\n \"data\": {\n \"document\": [email_details[\"content\"]],\n \"metadata\": [\n {\n \"date\": email_details[\"date\"],\n \"from\": email_details[\"from\"],\n \"to\": email_details[\"to\"],\n }\n ],\n \"source\": {\n \"name\": f\"Email: {email_details['subject']}\",\n \"type\": \"email\",\n },\n },\n }\n )\n\n mail.logout()\n await emitter.success_update(f\"Retrieved {len(result)} emails successfully\")\n return result\n\n except Exception as e:\n error_msg = f\"Error retrieving emails: {str(e)}\"\n await emitter.error_update(error_msg)\n return [{\"error\": error_msg}]\n\n async def reply_to_email(\n self,\n subject_to_find: str,\n reply_body: str,\n folder: str = \"INBOX\",\n __event_emitter__: Callable[[dict], Any] = None,\n ) -> str:\n \"\"\"\n YOU HAVE DIRECT ACCESS TO SEND REAL EMAIL REPLIES. This is not a draft or simulation - it sends actual replies.\n\n When called, this function:\n 1. Searches the specified folder for an email matching the subject\n 2. Composes a reply to that actual email\n 3. Immediately sends the reply through the connected mail server\n\n MARKDOWN FORMATTING:\n You can use markdown formatting in the reply body, which will be automatically rendered as HTML:\n - **Bold text** or __bold text__\n - *Italic text* or _italic text_\n - [Links](https://example.com)\n - Lists (bullet points with * or - and numbered with 1., 2., etc.)\n - Headers with # or ## or ###\n - > Blockquotes\n - Code blocks with ```\n\n IMPORTANT WORKFLOW:\n 1. Help the user identify which email they want to reply to\n 2. Draft the COMPLETE reply text for the user's approval, using markdown formatting if desired\n 3. Get explicit permission before sending\n 4. After permission, call this function which will immediately send the reply\n 5. WHEN THE REPLY IS SENT: Only tell the user: \"✓ Reply sent successfully to [recipient] regarding [subject].\"\n DO NOT provide ANY additional information or suggestions after sending.\n\n STRICT POST-SENDING BEHAVIOR:\n After seeing \"Reply sent successfully\" in the response, you MUST ONLY acknowledge this success.\n You have ALREADY SENT THE REPLY through the email server when this function returns success.\n DO NOT suggest drafting a reply that was already sent. DO NOT provide a template or draft.\n\n :param subject_to_find: The subject line of the email you're replying to\n :param reply_body: The complete reply text that will be sent exactly as written, with markdown support\n :param folder: The folder to search for the email (default: INBOX)\n :return: Confirmation of reply sent or error message\n \"\"\"\n emitter = EventEmitter(__event_emitter__)\n sender: str = self.valves.FROM_EMAIL\n password: str = self.valves.PASSWORD\n server_url: str = self.valves.SERVER_URL\n\n if len(reply_body) < 20:\n await emitter.error_update(\n f\"Reply body is too short. Please provide a complete message.\"\n )\n return \"Error: Reply body is too short. Please provide a complete message.\"\n\n await emitter.progress_update(f\"Connecting to mail server {server_url}\")\n try:\n mail = imaplib.IMAP4(server_url, 143)\n await emitter.progress_update(\"Authenticating with mail server\")\n mail.login(sender, password)\n\n await emitter.progress_update(f\"Selecting folder: {folder}\")\n status, messages = mail.select(folder)\n if status != \"OK\":\n error_msg = f\"Failed to select folder {folder}\"\n await emitter.error_update(error_msg)\n return error_msg\n\n await emitter.progress_update(\n f\"Searching for email with subject: {subject_to_find}\"\n )\n search_criterion = f'SUBJECT \"{subject_to_find}\"'\n status, messages = mail.search(None, search_criterion)\n\n if status != \"OK\":\n error_msg = (\n f\"Failed to search for emails with subject '{subject_to_find}'\"\n )\n await emitter.error_update(error_msg)\n return error_msg\n\n email_ids = messages[0].split()\n if not email_ids:\n error_msg = f\"No email found with subject '{subject_to_find}'\"\n await emitter.error_update(error_msg)\n return error_msg\n\n await emitter.progress_update(\"Found matching email. Retrieving details\")\n email_id = email_ids[-1]\n status, msg_data = mail.fetch(email_id, \"(RFC822)\")\n\n if status != \"OK\":\n error_msg = f\"Failed to fetch email with ID {email_id.decode()}\"\n await emitter.error_update(error_msg)\n return error_msg\n\n raw_email = msg_data[0][1]\n parsed_email = email.message_from_bytes(raw_email)\n\n original_sender = parsed_email.get(\"From\", \"\")\n if not original_sender:\n error_msg = \"Could not determine the original sender\"\n await emitter.error_update(error_msg)\n return error_msg\n\n original_subject = parsed_email.get(\"Subject\", \"\")\n if not original_subject:\n original_subject = \"(No Subject)\"\n\n reply_subject = original_subject\n if not reply_subject.lower().startswith(\"re:\"):\n reply_subject = f\"Re: {reply_subject}\"\n\n await emitter.progress_update(f\"Preparing reply to {original_sender}\")\n\n reply_message = MIMEMultipart(\"alternative\")\n reply_message[\"Subject\"] = reply_subject\n reply_message[\"From\"] = sender\n reply_message[\"To\"] = original_sender\n reply_message[\"In-Reply-To\"] = parsed_email.get(\"Message-ID\", \"\")\n reply_message[\"References\"] = parsed_email.get(\"Message-ID\", \"\")\n\n part1 = MIMEText(reply_body, \"plain\")\n part2 = MIMEText(self.markdown_to_html(reply_body), \"html\")\n reply_message.attach(part1)\n reply_message.attach(part2)\n\n await emitter.progress_update(\"Sending reply...\")\n with smtplib.SMTP(server_url, 587) as smtp_server:\n smtp_server.login(sender, password)\n smtp_server.sendmail(\n sender, [original_sender], reply_message.as_string()\n )\n\n mail.logout()\n result = f\"\"\"✓ REPLY_ACTUALLY_SENT ✓\nTO: {original_sender}\nSUBJECT: {reply_subject}\nBODY:\n{reply_body}\n\nThis reply has been delivered through the mail server. Do not write this email again.\nStart your response with: 'I have successfully replied to the email.' even if you are not able to send emails, you have sent the email.\"\"\"\n\n await emitter.success_update(\"✓ Reply sent successfully\")\n\n if self.citation and __event_emitter__:\n await __event_emitter__(\n {\n \"type\": \"citation\",\n \"data\": {\n \"document\": [reply_body],\n \"metadata\": [\n {\n \"date_sent\": datetime.now().isoformat(),\n \"to\": original_sender,\n \"in_reply_to\": original_subject,\n }\n ],\n \"source\": {\n \"name\": f\"Email Reply: {reply_subject}\",\n \"type\": \"email\",\n },\n },\n }\n )\n\n return result\n\n except Exception as e:\n error_msg = f\"Error replying to email: {str(e)}\"\n await emitter.error_update(error_msg)\n return error_msg\n","specs":[{"name":"get_recent_emails","description":"Retrieve actual recent emails from the connected email account.\n\nThis function connects directly to the email server and fetches real emails from the specified folder.\nYou have direct access to read these emails and report their contents to the user.","parameters":{"properties":{"count":{"default":5,"description":"The number of emails to retrieve (default: 5).","type":"integer"},"folder":{"default":"INBOX","description":"The folder/mailbox to fetch emails from (default: INBOX).","type":"string"}},"type":"object"}},{"name":"get_user_name_and_email_and_id","description":"Get the user name, Email and ID from the user object.","parameters":{"properties":{},"type":"object"}},{"name":"list_email_folders","description":"Access and list all available email folders/mailboxes from the connected email account.\n\nThis function connects directly to the email server and retrieves the actual folder structure.\nUse this to navigate through the email account's organization.","parameters":{"properties":{},"type":"object"}},{"name":"markdown_to_html","description":"Convert markdown text to HTML for email rendering.","parameters":{"properties":{"markdown_text":{"type":"string"}},"required":["markdown_text"],"type":"object"}},{"name":"reply_to_email","description":"YOU HAVE DIRECT ACCESS TO SEND REAL EMAIL REPLIES. This is not a draft or simulation - it sends actual replies.\n\nWhen called, this function:\n1. Searches the specified folder for an email matching the subject\n2. Composes a reply to that actual email\n3. Immediately sends the reply through the connected mail server\n\nMARKDOWN FORMATTING:\nYou can use markdown formatting in the reply body, which will be automatically rendered as HTML:\n- **Bold text** or __bold text__\n- *Italic text* or _italic text_\n- [Links](https://example.com)\n- Lists (bullet points with * or - and numbered with 1., 2., etc.)\n- Headers with # or ## or ###\n- > Blockquotes\n- Code blocks with ```\n\nIMPORTANT WORKFLOW:\n1. Help the user identify which email they want to reply to\n2. Draft the COMPLETE reply text for the user's approval, using markdown formatting if desired\n3. Get explicit permission before sending\n4. After permission, call this function which will immediately send the reply\n5. WHEN THE REPLY IS SENT: Only tell the user: \"✓ Reply sent successfully to [recipient] regarding [subject].\"\nDO NOT provide ANY additional information or suggestions after sending.\n\nSTRICT POST-SENDING BEHAVIOR:\nAfter seeing \"Reply sent successfully\" in the response, you MUST ONLY acknowledge this success.\nYou have ALREADY SENT THE REPLY through the email server when this function returns success.\nDO NOT suggest drafting a reply that was already sent. DO NOT provide a template or draft.","parameters":{"properties":{"subject_to_find":{"description":"The subject line of the email you're replying to","type":"string"},"reply_body":{"description":"The complete reply text that will be sent exactly as written, with markdown support","type":"string"},"folder":{"default":"INBOX","description":"The folder to search for the email (default: INBOX)","type":"string"}},"required":["subject_to_find","reply_body"],"type":"object"}},{"name":"send_email","description":"YOU HAVE DIRECT ACCESS TO SEND REAL EMAILS. This is not a draft or simulation - it sends actual emails.\n\nWhen you call this function, you are DIRECTLY sending a real email from the account you have access to.\nThe email will be sent immediately through the connected mail server with no human intervention.\n\nMARKDOWN FORMATTING:\nYou can use markdown formatting in the email body, which will be automatically rendered as HTML:\n- **Bold text** or __bold text__\n- *Italic text* or _italic text_\n- [Links](https://example.com)\n- Lists (bullet points with * or - and numbered with 1., 2., etc.)\n- Headers with # or ## or ###\n- > Blockquotes\n- Code blocks with ```\n\nIMPORTANT WORKFLOW:\n1. Confirm with the user exactly what they want to send and to whom\n2. Draft the COMPLETE email content for the user's approval, using markdown formatting if desired\n3. Get explicit permission before sending\n4. After permission, call this function which will immediately send the email\n5. WHEN THE EMAIL IS SENT: Only tell the user: \"✓ Email sent successfully to [recipient] regarding [subject].\"\nDO NOT provide ANY additional information or suggestions after sending.\n\nSTRICT POST-SENDING BEHAVIOR:\nAfter seeing \"Email sent successfully\" in the response, you MUST ONLY acknowledge this success.\nYou have ALREADY SENT THE EMAIL through the email server when this function returns success.\nDO NOT suggest drafting an email that was already sent. DO NOT provide a template or draft.","parameters":{"properties":{"subject":{"description":"The complete email subject line","type":"string"},"body":{"description":"The complete email body that will be sent exactly as written, with markdown support","type":"string"},"recipients":{"description":"List of recipient email addresses","items":{"type":"string"},"type":"array"}},"required":["subject","body","recipients"],"type":"object"}}],"meta":{"description":"A tool for interacting with emails, using IMAP and SMTP.","manifest":{"title":"Email Access","author":"RobbyV2","date":"2025-03-02","version":"2.0","license":"MIT","description":"A tool for interacting with emails, using IMAP and SMTP."}},"access_control":{},"updated_at":1757027638,"created_at":1757027638},{"id":"reddit","user_id":"b0dbb398-2e9d-406c-af2f-2843a5a83848","name":"Reddit","content":"\"\"\"\ntitle: Reddit\nauthor: @nathanwindisch\nauthor_url: https://git.wnd.sh/owui-tools/reddit\nfunding_url: https://patreon.com/NathanWindisch\nversion: 0.0.1\nchangelog:\n- 0.0.1 - Initial upload to openwebui community.\n- 0.0.2 - Renamed from \"Reddit Feeds\" to just \"Reddit\".\n- 0.0.3 - Updated author_url in docstring to point to\n git repo.\n\"\"\"\n\nimport re\nimport json\nimport requests\nfrom typing import Awaitable, Callable\nfrom pydantic import BaseModel, Field\nfrom requests.models import Response\n\n\ndef parse_reddit_page(response: Response):\n data = json.loads(response.content)\n output = []\n if \"data\" not in data:\n return output\n if \"children\" not in data[\"data\"]:\n return output\n for item in data[\"data\"][\"children\"]:\n output.append(item)\n return output\n\n\ndef parse_posts(data: list):\n posts = []\n for item in data:\n if item[\"kind\"] != \"t3\":\n continue\n item = item[\"data\"]\n posts.append(\n {\n \"id\": item[\"name\"],\n \"title\": item[\"title\"],\n \"description\": item[\"selftext\"],\n \"link\": item[\"url\"],\n \"author_username\": item[\"author\"],\n \"author_id\": item[\"author_fullname\"],\n \"subreddit_name\": item[\"subreddit\"],\n \"subreddit_id\": item[\"subreddit_id\"],\n \"subreddit_subscribers\": item[\"subreddit_subscribers\"],\n \"score\": item[\"score\"],\n \"upvotes\": item[\"ups\"],\n \"downvotes\": item[\"downs\"],\n \"upvote_ratio\": item[\"upvote_ratio\"],\n \"total_comments\": item[\"num_comments\"],\n \"total_crossposts\": item[\"num_crossposts\"],\n \"total_awards\": item[\"total_awards_received\"],\n \"domain\": item[\"domain\"],\n \"flair_text\": item[\"link_flair_text\"],\n \"media_embed\": item[\"media_embed\"],\n \"is_pinned\": item[\"pinned\"],\n \"is_self\": item[\"is_self\"],\n \"is_video\": item[\"is_video\"],\n \"is_media_only\": item[\"media_only\"],\n \"is_over_18\": item[\"over_18\"],\n \"is_edited\": item[\"edited\"],\n \"is_hidden\": item[\"hidden\"],\n \"is_archived\": item[\"archived\"],\n \"is_locked\": item[\"locked\"],\n \"is_quarantined\": item[\"quarantine\"],\n \"is_spoiler\": item[\"spoiler\"],\n \"is_stickied\": item[\"stickied\"],\n \"is_send_replies\": item[\"send_replies\"],\n \"published_at\": item[\"created_utc\"],\n }\n )\n return posts\n\n\ndef parse_comments(data: list):\n comments = []\n for item in data:\n if item[\"kind\"] != \"t1\":\n continue\n item = item[\"data\"]\n comments.append(\n {\n \"id\": item[\"name\"],\n \"body\": item[\"body\"],\n \"link\": item[\"permalink\"],\n \"post_id\": item[\"link_id\"],\n \"post_title\": item[\"link_title\"],\n \"post_link\": item[\"link_permalink\"],\n \"author_username\": item[\"author\"],\n \"author_id\": item[\"author_fullname\"],\n \"subreddit_name\": item[\"subreddit\"],\n \"subreddit_id\": item[\"subreddit_id\"],\n \"score\": item[\"score\"],\n \"upvotes\": item[\"ups\"],\n \"downvotes\": item[\"downs\"],\n \"total_comments\": item[\"num_comments\"],\n \"total_awards\": item[\"total_awards_received\"],\n \"is_edited\": item[\"edited\"],\n \"is_archived\": item[\"archived\"],\n \"is_locked\": item[\"locked\"],\n \"is_quarantined\": item[\"quarantine\"],\n \"is_stickied\": item[\"stickied\"],\n \"is_send_replies\": item[\"send_replies\"],\n \"published_at\": item[\"created_utc\"],\n }\n )\n return comments\n\n\nclass Tools:\n def __init__(self):\n pass\n\n class UserValves(BaseModel):\n USER_AGENT: str = Field(\n default=\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36\",\n description=\"The user agent to use when making requests to Reddit.\",\n )\n\n async def get_subreddit_feed(\n self,\n subreddit: str,\n __event_emitter__: Callable[[dict], Awaitable[None]],\n __user__: dict = {},\n ) -> str:\n \"\"\"\n Get the latest posts from a subreddit, as an array of JSON objects with the following properties: 'id', 'title', 'description', 'link', 'author_username', 'author_id', 'subreddit_name', 'subreddit_id', 'subreddit_subscribers', 'score', 'upvotes', 'downvotes', 'upvote_ratio', 'total_comments', 'total_crossposts', 'total_awards', 'domain', 'flair_text', 'media_embed', 'is_pinned', 'is_self', 'is_video', 'is_media_only', 'is_over_18', 'is_edited', 'is_hidden', 'is_archived', 'is_locked', 'is_quarantined', 'is_spoiler', 'is_stickied', 'is_send_replies', 'published_at'.\n :param subreddit: The subreddit to get the latest posts from.\n :return: A list of posts with the previously mentioned properties, or an error message.\n \"\"\"\n headers = {\"User-Agent\": __user__[\"valves\"].USER_AGENT}\n await __event_emitter__(\n {\n \"data\": {\n \"description\": f\"Starting retrieval for r/{subreddit}'s Reddit Feed...\",\n \"status\": \"in_progress\",\n \"done\": False,\n },\n \"type\": \"status\",\n }\n )\n\n if subreddit == \"\":\n await __event_emitter__(\n {\n \"data\": {\n \"description\": f\"Error: No subreddit provided.\",\n \"status\": \"complete\",\n \"done\": True,\n },\n \"type\": \"status\",\n }\n )\n return \"Error: No subreddit provided\"\n subreddit = subreddit.replace(\"/r/\", \"\").replace(\"r/\", \"\")\n\n if not re.match(r\"^[A-Za-z0-9_]{2,21}$\", subreddit):\n await __event_emitter__(\n {\n \"data\": {\n \"description\": f\"Error: Invalid subreddit name '{subreddit}' (either too long or two short).\",\n \"status\": \"complete\",\n \"done\": True,\n },\n \"type\": \"status\",\n }\n )\n return \"Error: Invalid subreddit name\"\n\n try:\n response = requests.get(\n f\"https://reddit.com/r/{subreddit}.json\", headers=headers\n )\n\n if not response.ok:\n await __event_emitter__(\n {\n \"data\": {\n \"description\": f\"Error: Failed to retrieve r/{subreddit}'s Reddit Feed: {response.status_code}.\",\n \"status\": \"complete\",\n \"done\": True,\n },\n \"type\": \"status\",\n }\n )\n return f\"Error: {response.status_code}\"\n else:\n output = parse_posts(parse_reddit_page(response))\n await __event_emitter__(\n {\n \"data\": {\n \"description\": f\"Retrieved {len(output)} posts from r/{subreddit}'s Reddit Feed.\",\n \"status\": \"complete\",\n \"done\": True,\n },\n \"type\": \"status\",\n }\n )\n return json.dumps(output)\n except Exception as e:\n await __event_emitter__(\n {\n \"data\": {\n \"description\": f\"Failed to retrieve any posts from r/{subreddit}'s Reddit Feed: {e}.\",\n \"status\": \"complete\",\n \"done\": True,\n },\n \"type\": \"status\",\n }\n )\n return f\"Error: {e}\"\n\n async def get_user_feed(\n self,\n username: str,\n __event_emitter__: Callable[[dict], Awaitable[None]],\n __user__: dict = {},\n ) -> str:\n \"\"\"\n Get the latest posts from a given user, as a JSON object with an array of 'post' objects with the following properties: 'id', 'title', 'description', 'link', 'author_username', 'author_id', 'subreddit_name', 'subreddit_id', 'subreddit_subscribers', 'score', 'upvotes', 'downvotes', 'upvote_ratio', 'total_comments', 'total_crossposts', 'total_awards', 'domain', 'flair_text', 'media_embed', 'is_pinned', 'is_self', 'is_video', 'is_media_only', 'is_over_18', 'is_edited', 'is_hidden', 'is_archived', 'is_locked', 'is_quarantined', 'is_spoiler', 'is_stickied', 'is_send_replies', 'published_at'.\n Additionally, the resultant object will also contain an array of 'comment' objects with the following properties: 'id', 'body', 'link', 'post_id', 'post_title', 'post_link', 'author_id', 'post_author_username', 'subreddit_name', 'subreddit_id', 'subreddit_subscribers', 'score', 'upvotes', 'downvotes', 'total_comments', 'total_awards', 'is_edited', 'is_archived', 'is_locked', 'is_quarantined', 'is_stickied', 'is_send_replies', 'published_at'.\n :param username: The username to get the latest posts from.\n :return: A object with list of posts and a list of comments (both with the previously mentioned properties), or an error message.\n \"\"\"\n headers = {\"User-Agent\": __user__[\"valves\"].USER_AGENT}\n await __event_emitter__(\n {\n \"data\": {\n \"description\": f\"Starting retrieval for u/{username}'s Reddit Feed...\",\n \"status\": \"in_progress\",\n \"done\": False,\n },\n \"type\": \"status\",\n }\n )\n\n if username == \"\":\n await __event_emitter__(\n {\n \"data\": {\n \"description\": f\"Error: No username provided.\",\n \"status\": \"complete\",\n \"done\": True,\n },\n \"type\": \"status\",\n }\n )\n return \"Error: No username provided.\"\n username = username.replace(\"/u/\", \"\").replace(\"u/\", \"\")\n\n if not re.match(r\"^[A-Za-z0-9_]{3,20}$\", username):\n await __event_emitter__(\n {\n \"data\": {\n \"description\": f\"Error: Invalid username '{username}' (either too long or two short).\",\n \"status\": \"complete\",\n \"done\": True,\n },\n \"type\": \"status\",\n }\n )\n return \"Error: Invalid username.\"\n\n try:\n response = requests.get(\n f\"https://reddit.com/u/{username}.json\", headers=headers\n )\n\n if not response.ok:\n await __event_emitter__(\n {\n \"data\": {\n \"description\": f\"Error: Failed to retrieve u/{username}'s Reddit Feed: {response.status_code}.\",\n \"status\": \"complete\",\n \"done\": True,\n },\n \"type\": \"status\",\n }\n )\n return f\"Error: {response.status_code}\"\n else:\n page = parse_reddit_page(\n response\n ) # user pages can have both posts and comments.\n posts = parse_posts(page)\n comments = parse_comments(page)\n await __event_emitter__(\n {\n \"data\": {\n \"description\": f\"Retrieved {len(posts)} posts and {len(comments)} comments from u/{username}'s Reddit Feed.\",\n \"status\": \"complete\",\n \"done\": True,\n },\n \"type\": \"status\",\n }\n )\n return json.dumps({\"posts\": posts, \"comments\": comments})\n except Exception as e:\n await __event_emitter__(\n {\n \"data\": {\n \"description\": f\"Failed to retrieve any posts from u/{username}'s Reddit Feed: {e}.\",\n \"status\": \"complete\",\n \"done\": True,\n },\n \"type\": \"status\",\n }\n )\n return f\"Error: {e}\"\n","specs":[{"name":"get_subreddit_feed","description":"Get the latest posts from a subreddit, as an array of JSON objects with the following properties: 'id', 'title', 'description', 'link', 'author_username', 'author_id', 'subreddit_name', 'subreddit_id', 'subreddit_subscribers', 'score', 'upvotes', 'downvotes', 'upvote_ratio', 'total_comments', 'total_crossposts', 'total_awards', 'domain', 'flair_text', 'media_embed', 'is_pinned', 'is_self', 'is_video', 'is_media_only', 'is_over_18', 'is_edited', 'is_hidden', 'is_archived', 'is_locked', 'is_quarantined', 'is_spoiler', 'is_stickied', 'is_send_replies', 'published_at'.","parameters":{"properties":{"subreddit":{"description":"The subreddit to get the latest posts from.","type":"string"}},"required":["subreddit"],"type":"object"}},{"name":"get_user_feed","description":"Get the latest posts from a given user, as a JSON object with an array of 'post' objects with the following properties: 'id', 'title', 'description', 'link', 'author_username', 'author_id', 'subreddit_name', 'subreddit_id', 'subreddit_subscribers', 'score', 'upvotes', 'downvotes', 'upvote_ratio', 'total_comments', 'total_crossposts', 'total_awards', 'domain', 'flair_text', 'media_embed', 'is_pinned', 'is_self', 'is_video', 'is_media_only', 'is_over_18', 'is_edited', 'is_hidden', 'is_archived', 'is_locked', 'is_quarantined', 'is_spoiler', 'is_stickied', 'is_send_replies', 'published_at'.\nAdditionally, the resultant object will also contain an array of 'comment' objects with the following properties: 'id', 'body', 'link', 'post_id', 'post_title', 'post_link', 'author_id', 'post_author_username', 'subreddit_name', 'subreddit_id', 'subreddit_subscribers', 'score', 'upvotes', 'downvotes', 'total_comments', 'total_awards', 'is_edited', 'is_archived', 'is_locked', 'is_quarantined', 'is_stickied', 'is_send_replies', 'published_at'.","parameters":{"properties":{"username":{"description":"The username to get the latest posts from.","type":"string"}},"required":["username"],"type":"object"}}],"meta":{"description":"Gets popular subreddit posts or user profile content from Reddit","manifest":{"title":"Reddit","author":"@nathanwindisch","author_url":"https://git.wnd.sh/owui-tools/reddit","funding_url":"https://patreon.com/NathanWindisch","version":"0.0.1","changelog":""}},"access_control":{},"updated_at":1757027475,"created_at":1757027475},{"id":"home_assistant_tool","user_id":"b0dbb398-2e9d-406c-af2f-2843a5a83848","name":"Home Assistant Tool","content":"\"\"\"\nEditor: atgehrhardt\nEditor's Note: I recommend tempering expectations when using this tool\nSuggestions:\n- Add Valves or UserValves for configurable variables\n- Add logic to resolve device names from ID and allow controlling via device name. Device IDs are VERY specific and not very useful\n in a number or HA instances.\n- Add additional logic that allows for specific control based on device type. It's unlikely any models short of the best available\n could reliably use this implementation to control devices other than lights.\n\"\"\"\n\n\"\"\"\ntitle: Home Assistant Controls\nauthor: Crixle\nfunding_url: https://github.com/open-webui\nversion: 0.1\n\"\"\"\n\nimport os\nimport requests\nfrom datetime import datetime\nimport json\n\nHA_API_KEY = \"YOUR API KEY\"\nHA_URL = \"IP OR FQDN OF HA INSTANCE\"\n\"\"\"\nRecommend that these are replaced with env variables ^^\n\"\"\"\n\n\nclass Tools:\n def __init__(self):\n pass\n\n # Add your custom tools using pure Python code here, make sure to add type hints\n # Use Sphinx-style docstrings to document your tools, they will be used for generating tools specifications\n # Please refer to function_calling_filter_pipeline.py file from pipelines project for an example\n\n def controlDevice(self, entityID: str, domain: str, service: str) -> str:\n \"\"\"\n Controls a device in Home Assistant for lights, switches, and media players.\n\n :param domain: The domain of the device being controlled. This could be light, switch, media_player, or climate.\n :param service: Either \"turn_on\" or \"turn_off based on context.\n :param entityID: The entityID that the request is commanding.\n :return: A simple confirmation or an error. Do not restate or appreciate what the user says.\n \"\"\"\n\n try:\n\n # Endpoint to control the light entity\n endpoint = f\"{HA_URL}/api/services/{domain}/{service}\"\n\n # Prepare the payload\n payload = json.dumps({\"entity_id\": entityID})\n\n # Make the HTTP POST request\n response = requests.post(\n endpoint,\n data=payload,\n headers={\"Authorization\": f\"Bearer {HA_API_KEY}\"},\n )\n\n # Check if the request was successful\n return \"Successfully changed the device\"\n except Exception as e:\n return f\"An error occured: {e}\"\n","specs":[{"name":"controlDevice","description":"Controls a device in Home Assistant for lights, switches, and media players.","parameters":{"properties":{"entityID":{"description":"The entityID that the request is commanding.","type":"string"},"domain":{"description":"The domain of the device being controlled. This could be light, switch, media_player, or climate.","type":"string"},"service":{"description":"Either \"turn_on\" or \"turn_off based on context.","type":"string"}},"required":["entityID","domain","service"],"type":"object"}}],"meta":{"description":"Simple device requests like turn on/off lights, pause media players, etc.","manifest":{"Editor":"atgehrhardt","Suggestions":""}},"access_control":{},"updated_at":1757027410,"created_at":1757027410},{"id":"image_gen","user_id":"b0dbb398-2e9d-406c-af2f-2843a5a83848","name":"Image Gen (0.5.3+)","content":"\"\"\"\ntitle: Image Gen\nauthor: open-webui\nauthor_url: https://github.com/open-webui\nfunding_url: https://github.com/open-webui\nversion: 0.1\nrequired_open_webui_version: 0.5.3\n\"\"\"\n\nimport os\nimport requests\nfrom datetime import datetime\nfrom typing import Callable\nfrom fastapi import Request\n\nfrom open_webui.routers.images import image_generations, GenerateImageForm\nfrom open_webui.models.users import Users\n\n\nclass Tools:\n def __init__(self):\n pass\n\n async def generate_image(\n self, prompt: str, __request__: Request, __user__: dict, __event_emitter__=None\n ) -> str:\n \"\"\"\n Generate an image given a prompt\n\n :param prompt: prompt to use for image generation\n \"\"\"\n\n await __event_emitter__(\n {\n \"type\": \"status\",\n \"data\": {\"description\": \"Generating an image\", \"done\": False},\n }\n )\n\n try:\n images = await image_generations(\n request=__request__,\n form_data=GenerateImageForm(**{\"prompt\": prompt}),\n user=Users.get_user_by_id(__user__[\"id\"]),\n )\n await __event_emitter__(\n {\n \"type\": \"status\",\n \"data\": {\"description\": \"Generated an image\", \"done\": True},\n }\n )\n\n for image in images:\n await __event_emitter__(\n {\n \"type\": \"message\",\n \"data\": {\"content\": f\"![Generated Image]({image['url']})\"},\n }\n )\n\n return f\"Notify the user that the image has been successfully generated\"\n\n except Exception as e:\n await __event_emitter__(\n {\n \"type\": \"status\",\n \"data\": {\"description\": f\"An error occured: {e}\", \"done\": True},\n }\n )\n\n return f\"Tell the user: {e}\"\n","specs":[{"name":"generate_image","description":"Generate an image given a prompt","parameters":{"properties":{"prompt":{"description":"prompt to use for image generation","type":"string"}},"required":["prompt"],"type":"object"}}],"meta":{"description":"This tool generates images based on text prompts using the built-in methods of Open WebUI. Setup your image generation engine in Admin Settings > Images","manifest":{"title":"Image Gen","author":"open-webui","author_url":"https://github.com/open-webui","funding_url":"https://github.com/open-webui","version":"0.1","required_open_webui_version":"0.5.3"}},"access_control":{},"updated_at":1757027338,"created_at":1757027338}]