├── generate_image_tool.py ├── img2Img_comfy_tool.py ├── img2img_sysprompt.txt ├── readme.md └── txt2img_sysprompt.txt /generate_image_tool.py: -------------------------------------------------------------------------------- 1 | """ 2 | title: Txt2Img Comfy Tool 3 | Author: Patrick Williams 4 | author_url: https://reticulated.net 5 | git_url: https://github.com/pwillia7/open-webui-tools.git 6 | description: Submits a txt2img node-based ComfyUI workflow over HTTPS by replacing the %%PROMPT%% placeholder with the user's input. It then listens on the WebSocket until generation completes and retrieves final images from /history. 7 | required_open_webui_version: 0.4.0 8 | requirements: requests, websocket-client 9 | version: 1.0.0 10 | license: MIT 11 | """ 12 | 13 | import uuid 14 | import json 15 | import base64 16 | import requests 17 | import asyncio 18 | import websockets 19 | import logging 20 | from typing import Dict, Any, Optional, Callable, Awaitable 21 | from pydantic import BaseModel, Field 22 | 23 | # Configure logging 24 | logging.basicConfig( 25 | level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s" 26 | ) 27 | 28 | 29 | class Tools: 30 | """ 31 | [LLM Usage Directive]: 32 | - **Purpose**: This tool generates images using a node-based ComfyUI workflow. 33 | - **When to Use**: 34 | e.g. "Generate an image of a futuristic city at sunset." 35 | - **How to Use**: 36 | Call run_comfyui_workflow(prompt_text=USER_PROMPT) directly. 37 | - **Error Handling**: 38 | Returns error messages verbatim. 39 | """ 40 | 41 | # All configuration now lives under Valves (admin-only settings) 42 | class Valves(BaseModel): 43 | Api_Key: Optional[str] = Field( 44 | None, 45 | description="The API token for authenticating with ComfyUI. Must be set in the Open-WebUI admin interface.", 46 | ) 47 | ComfyUI_Server: Optional[str] = Field( 48 | "ptkwilliams.ddns.net:8443", 49 | description="The address of the ComfyUI server, e.g. 'localhost:8188'. Must be set in Open-WebUI.", 50 | ) 51 | Workflow_URL: Optional[str] = Field( 52 | "https://gist.githubusercontent.com/pwillia7/9fe756338c7d35eba130c68408b705f4/raw/4a429e1ede948e02e405e3a046b2eb85546f1c0f/fluxgen", 53 | description="The URL where the ComfyUI workflow JSON is hosted.", 54 | ) 55 | debug_mode: bool = Field( 56 | default=False, 57 | description="Enable additional debug output for troubleshooting.", 58 | ) 59 | 60 | def __init__(self) -> None: 61 | self.valves = self.Valves() 62 | 63 | if not self.valves.ComfyUI_Server: 64 | raise ValueError( 65 | "ComfyUI server address is not set in Valves. Please configure it in Open-WebUI." 66 | ) 67 | # Use the exact value from the valve 68 | self.server_address = self.valves.ComfyUI_Server 69 | 70 | if not self.valves.Workflow_URL: 71 | raise ValueError( 72 | "Workflow URL is not set in Valves. Please configure it in Open-WebUI." 73 | ) 74 | try: 75 | response = requests.get(self.valves.Workflow_URL, timeout=30) 76 | response.raise_for_status() 77 | self.workflow_template: Dict[str, Any] = response.json() 78 | except Exception as e: 79 | raise ValueError( 80 | f"Failed to load workflow from URL {self.valves.Workflow_URL}: {e}" 81 | ) 82 | 83 | def _replace_placeholders(self, data: dict, placeholders: Dict[str, str]) -> dict: 84 | """ 85 | Recursively replace '%%PLACEHOLDER%%' in string fields within a dict. 86 | """ 87 | for key, value in data.items(): 88 | if isinstance(value, str): 89 | for ph, replacement in placeholders.items(): 90 | value = value.replace(ph, replacement) 91 | data[key] = value 92 | elif isinstance(value, dict): 93 | data[key] = self._replace_placeholders(value, placeholders) 94 | elif isinstance(value, list): 95 | data[key] = [ 96 | ( 97 | self._replace_placeholders(item, placeholders) 98 | if isinstance(item, dict) 99 | else ( 100 | self._replace_string_in_list_item(item, placeholders) 101 | if isinstance(item, str) 102 | else item 103 | ) 104 | ) 105 | for item in value 106 | ] 107 | return data 108 | 109 | def _replace_string_in_list_item( 110 | self, text: str, placeholders: Dict[str, str] 111 | ) -> str: 112 | for ph, replacement in placeholders.items(): 113 | text = text.replace(ph, replacement) 114 | return text 115 | 116 | def _queue_prompt(self, workflow: dict, client_id: str) -> str: 117 | """ 118 | Submit the workflow to the ComfyUI API. 119 | The API token is sent as a Bearer token in the Authorization header. 120 | """ 121 | if not self.valves.Api_Key: 122 | raise ValueError( 123 | "API token is not set in Valves. Please configure it in Open-WebUI." 124 | ) 125 | url = f"https://{self.server_address}/prompt" 126 | headers = {"Authorization": f"Bearer {self.valves.Api_Key}"} 127 | body = {"prompt": workflow, "client_id": client_id} 128 | 129 | logging.debug(f"Submitting API request to {url}") 130 | logging.debug(f"Request Headers: {headers}") 131 | logging.debug(f"Request Body: {json.dumps(body)}") 132 | 133 | resp = requests.post(url, json=body, headers=headers, timeout=30) 134 | logging.debug(f"Response Headers: {resp.headers}") 135 | logging.debug(f"Response Status Code: {resp.status_code}") 136 | logging.debug(f"Response Body: {resp.text}") 137 | 138 | resp.raise_for_status() 139 | return resp.json()["prompt_id"] 140 | 141 | async def run_comfyui_workflow( 142 | self, 143 | prompt_text: str, 144 | __event_emitter__: Optional[Callable[[Dict[str, Any]], Awaitable[None]]] = None, 145 | ) -> str: 146 | """ 147 | Execute the ComfyUI workflow by: 148 | 1) Replacing %%PROMPT%% in the workflow JSON with the user's prompt. 149 | 2) Submitting the prompt to /prompt. 150 | 3) Listening on the WebSocket until generation completes. 151 | 4) Fetching generated images from /history/. 152 | 5) Emitting them as chat messages. 153 | """ 154 | if not self.valves.Api_Key: 155 | raise ValueError( 156 | "API token is not set in Valves. Please configure it in Open-WebUI." 157 | ) 158 | 159 | import copy 160 | 161 | logging.debug(f"Using user prompt: {prompt_text}") 162 | 163 | workflow_copy = copy.deepcopy(self.workflow_template) 164 | updated_workflow = self._replace_placeholders( 165 | workflow_copy, {"%%PROMPT%%": prompt_text} 166 | ) 167 | if not isinstance(updated_workflow, dict): 168 | raise TypeError("Workflow must be a dict after placeholders are replaced.") 169 | 170 | client_id = str(uuid.uuid4()) 171 | 172 | # Build the WebSocket URL exactly as in the working tool. 173 | ws_url = f"wss://{self.valves.ComfyUI_Server}/ws?clientId={client_id}" 174 | if self.valves.Api_Key: 175 | ws_url += f"&token={self.valves.Api_Key}" 176 | 177 | logging.debug(f"Connecting WebSocket to: {ws_url}") 178 | if __event_emitter__: 179 | await __event_emitter__( 180 | { 181 | "type": "status", 182 | "data": {"description": "Connecting to ComfyUI...", "done": False}, 183 | } 184 | ) 185 | 186 | try: 187 | async with websockets.connect(ws_url) as ws: 188 | logging.debug("WebSocket connection established.") 189 | if __event_emitter__: 190 | await __event_emitter__( 191 | { 192 | "type": "status", 193 | "data": { 194 | "description": "Connected! Submitting workflow...", 195 | "done": False, 196 | }, 197 | } 198 | ) 199 | 200 | try: 201 | prompt_id = self._queue_prompt(updated_workflow, client_id) 202 | logging.debug(f"Workflow submitted. prompt_id={prompt_id}") 203 | if __event_emitter__: 204 | await __event_emitter__( 205 | { 206 | "type": "status", 207 | "data": { 208 | "description": "Workflow submitted. Waiting for generation...", 209 | "done": False, 210 | }, 211 | } 212 | ) 213 | except Exception as e: 214 | logging.error(f"Error submitting workflow: {e}") 215 | if __event_emitter__: 216 | await __event_emitter__( 217 | { 218 | "type": "status", 219 | "data": { 220 | "description": f"Workflow submission failed: {e}", 221 | "done": True, 222 | }, 223 | } 224 | ) 225 | return f"Error submitting workflow: {e}" 226 | 227 | try: 228 | async for raw_msg in ws: 229 | if isinstance(raw_msg, bytes): 230 | continue 231 | message_data = json.loads(raw_msg) 232 | msg_type = message_data.get("type", "") 233 | msg_info = message_data.get("data", {}) 234 | if ( 235 | msg_type == "executing" 236 | and msg_info.get("prompt_id") == prompt_id 237 | and msg_info.get("node") is None 238 | ): 239 | logging.debug( 240 | "ComfyUI signaled that generation is complete." 241 | ) 242 | break 243 | except Exception as e: 244 | logging.error(f"Error receiving WebSocket messages: {e}") 245 | except Exception as e: 246 | logging.error(f"WebSocket connection failed: {e}") 247 | if __event_emitter__: 248 | await __event_emitter__( 249 | { 250 | "type": "status", 251 | "data": { 252 | "description": f"Could not connect to ComfyUI WebSocket: {e}", 253 | "done": True, 254 | }, 255 | } 256 | ) 257 | return f"WebSocket connection error: {e}" 258 | 259 | if __event_emitter__: 260 | await __event_emitter__( 261 | { 262 | "type": "status", 263 | "data": { 264 | "description": "Generation complete. Retrieving images...", 265 | "done": False, 266 | }, 267 | } 268 | ) 269 | 270 | history_url = f"https://{self.valves.ComfyUI_Server}/history/{prompt_id}?token={self.valves.Api_Key}" 271 | try: 272 | resp = requests.get(history_url, timeout=30) 273 | resp.raise_for_status() 274 | history_data = resp.json() 275 | logging.debug(f"History data retrieved: {history_data}") 276 | except Exception as e: 277 | logging.error(f"Error fetching /history data: {e}") 278 | if __event_emitter__: 279 | await __event_emitter__( 280 | { 281 | "type": "status", 282 | "data": { 283 | "description": f"Error retrieving images: {e}", 284 | "done": True, 285 | }, 286 | } 287 | ) 288 | return f"Error retrieving images: {e}" 289 | 290 | if prompt_id not in history_data: 291 | if __event_emitter__: 292 | await __event_emitter__( 293 | { 294 | "type": "status", 295 | "data": { 296 | "description": "No history found for this prompt.", 297 | "done": True, 298 | }, 299 | } 300 | ) 301 | return "Workflow completed, but no history data was found for this prompt." 302 | 303 | outputs = history_data[prompt_id].get("outputs", {}) 304 | if not outputs: 305 | if __event_emitter__: 306 | await __event_emitter__( 307 | { 308 | "type": "status", 309 | "data": { 310 | "description": "No images found in workflow outputs.", 311 | "done": True, 312 | }, 313 | } 314 | ) 315 | return "Workflow completed, but no images were found in the outputs." 316 | 317 | image_count = 0 318 | for node_id, node_output in outputs.items(): 319 | images_list = node_output.get("images", []) 320 | for img_meta in images_list: 321 | image_count += 1 322 | filename = img_meta["filename"] 323 | subfolder = img_meta["subfolder"] 324 | folder_type = img_meta["type"] 325 | image_url = f"https://{self.valves.ComfyUI_Server}/view?filename={filename}&subfolder={subfolder}&type={folder_type}&token={self.valves.Api_Key}" 326 | if __event_emitter__: 327 | await __event_emitter__( 328 | { 329 | "type": "message", 330 | "data": { 331 | "content": f"**Generated Image #{image_count}:** ![Preview]({image_url})" 332 | }, 333 | } 334 | ) 335 | 336 | if __event_emitter__: 337 | await __event_emitter__( 338 | { 339 | "type": "status", 340 | "data": { 341 | "description": f"Workflow completed successfully. Retrieved {image_count} image(s).", 342 | "done": True, 343 | }, 344 | } 345 | ) 346 | 347 | return f"Workflow completed. Received {image_count} image(s)." 348 | -------------------------------------------------------------------------------- /img2Img_comfy_tool.py: -------------------------------------------------------------------------------- 1 | """ 2 | title: Img2Img Comfy Tool 3 | Author: Patrick Williams 4 | author_url: https://reticulated.net 5 | git_url: https://github.com/username/comfyui-workflow-runner.git 6 | description: Submits an img2img node-based ComfyUI workflow over HTTP, then listens on the WebSocket only to know when generation completes. Then retrieves final images from /history and optionally reorders them. 7 | required_open_webui_version: 0.4.0 8 | requirements: requests, websocket-client 9 | version: 1.3.1 10 | license: MIT 11 | """ 12 | 13 | import uuid 14 | import json 15 | import base64 16 | import requests 17 | import asyncio 18 | import websockets 19 | import logging 20 | from typing import Dict, Any, Optional, Callable, Awaitable 21 | from pydantic import BaseModel, Field 22 | 23 | # Configure logging 24 | logging.basicConfig( 25 | level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s" 26 | ) 27 | 28 | 29 | class Tools: 30 | """ 31 | - We do not receive images over WebSocket. We only detect completion (node=None or closure). 32 | - Then we gather final images from /history, reorder them if requested, and emit them. 33 | """ 34 | 35 | class Valves(BaseModel): 36 | Api_Key: Optional[str] = Field( 37 | None, 38 | description="API token for ComfyUI (if using ComfyUI-Login). If not needed, leave blank.", 39 | ) 40 | Server_Address: Optional[str] = Field( 41 | None, description="ComfyUI address, e.g. 'localhost:8443'." 42 | ) 43 | debug_mode: bool = Field(default=False, description="Extra debug logs if True.") 44 | workflow_file_url: Optional[str] = Field( 45 | default=None, 46 | description="URL of a JSON workflow. If not set, we use an inline default.", 47 | ) 48 | 49 | # New valve for reordering images by a comma-separated list of 1-based indices 50 | image_order_list: str = Field( 51 | default="", 52 | description=( 53 | "Comma-separated 1-based indexes to reorder final images. " 54 | "E.g. '3,1,4,2' to reorder images in that sequence. " 55 | "Out-of-range indices are skipped. If empty, use original order." 56 | ), 57 | ) 58 | 59 | # UI messages 60 | connecting_to_comfyui_msg: str = Field( 61 | default="Connecting to ComfyUI...", 62 | description="Shown when we first attempt WebSocket connect.", 63 | ) 64 | connected_submitting_workflow_msg: str = Field( 65 | default="Connected! Submitting workflow...", 66 | description="Shown once the socket is open & we post /prompt.", 67 | ) 68 | workflow_submitted_msg: str = Field( 69 | default="Workflow submitted. Waiting until ComfyUI forcibly closes...", 70 | description="Shown after the /prompt call returns OK.", 71 | ) 72 | comfyui_generation_complete_msg: str = Field( 73 | default="ComfyUI closed the connection or signaled completion; retrieving final images...", 74 | description="Shown once we detect the generation is done.", 75 | ) 76 | workflow_submission_failed_msg: str = Field( 77 | default="Workflow submission failed: {exception}", 78 | description="Error if we cannot submit to /prompt.", 79 | ) 80 | ws_connection_error_msg: str = Field( 81 | default="Could not connect to ComfyUI WebSocket: {exception}", 82 | description="Error if the WebSocket fails to connect or drops prematurely.", 83 | ) 84 | original_image_msg: str = Field( 85 | default="**Original Image:** ![Original]({url})", 86 | description="Displayed to show the user's original image in chat.", 87 | ) 88 | error_fetching_image_msg: str = Field( 89 | default="Error fetching image: {exception}", 90 | description="Error if we fail to download the user-provided image.", 91 | ) 92 | no_history_msg: str = Field( 93 | default="No history found for this prompt.", 94 | description="If /history has no record of the promptId.", 95 | ) 96 | no_images_workflow_msg: str = Field( 97 | default="No images found in workflow outputs.", 98 | description="If the final pipeline yields zero images in /history.", 99 | ) 100 | error_retrieving_images_msg: str = Field( 101 | default="Error retrieving images: {exception}", 102 | description="If /history retrieval fails.", 103 | ) 104 | workflow_completed_status_msg: str = Field( 105 | default="Workflow completed successfully. Retrieved {image_count} image(s).", 106 | description="Status message at the end of generation.", 107 | ) 108 | workflow_completed_return_msg: str = Field( 109 | default="Workflow completed. Received {image_count} image(s).", 110 | description="Return value after final success.", 111 | ) 112 | 113 | def __init__(self): 114 | self.valves = self.Valves() 115 | 116 | # Minimal inline default if no workflow_file_url is provided 117 | self.workflow_template: Dict[str, Any] = { 118 | "workflow": {"some_node": "default or empty workflow goes here"} 119 | } 120 | 121 | def _replace_placeholders(self, data: dict, placeholders: dict) -> dict: 122 | """Recursively replace placeholders like %%B64IMAGE%% or %%PROMPT%% in the workflow JSON.""" 123 | for k, v in data.items(): 124 | if isinstance(v, str): 125 | for ph, rp in placeholders.items(): 126 | v = v.replace(ph, rp) 127 | data[k] = v 128 | elif isinstance(v, dict): 129 | data[k] = self._replace_placeholders(v, placeholders) 130 | elif isinstance(v, list): 131 | new_list = [] 132 | for item in v: 133 | if isinstance(item, dict): 134 | new_list.append(self._replace_placeholders(item, placeholders)) 135 | elif isinstance(item, str): 136 | for ph, rp in placeholders.items(): 137 | item = item.replace(ph, rp) 138 | new_list.append(item) 139 | else: 140 | new_list.append(item) 141 | data[k] = new_list 142 | return data 143 | 144 | def _queue_prompt(self, workflow: dict, client_id: str) -> str: 145 | """ 146 | POST the workflow to /prompt with the same client_id used in the WebSocket. 147 | If self.valves.Api_Key is set, we append ?token=API_KEY to the URL. 148 | """ 149 | if not self.valves.Server_Address: 150 | raise ValueError("No ComfyUI server address set in valves.") 151 | 152 | base_url = f"https://{self.valves.Server_Address}/prompt" 153 | if self.valves.Api_Key: 154 | base_url += f"?token={self.valves.Api_Key}" 155 | 156 | body = {"prompt": workflow, "client_id": client_id} 157 | 158 | if self.valves.debug_mode: 159 | logging.debug(f"Posting workflow to: {base_url}") 160 | 161 | resp = requests.post(base_url, json=body, timeout=30) 162 | resp.raise_for_status() 163 | return resp.json().get("prompt_id", "") 164 | 165 | async def run_comfyui_img2img_workflow( 166 | self, 167 | image_url: str, 168 | prompt_text: str = "", 169 | target_node: str = "%%B64IMAGE%%", 170 | max_returned_images: int = 5, 171 | __event_emitter__: Optional[Callable[[Dict[str, Any]], Awaitable[None]]] = None, 172 | ) -> str: 173 | """ 174 | Steps: 175 | 1) Download the image 176 | 2) Possibly fetch .workflow_file_url 177 | 3) Insert placeholders 178 | 4) WebSocket with ?clientId=... 179 | 5) /prompt with that client_id 180 | 6) Wait for 'node=None' or forced close => generation done 181 | 7) /history to gather final images, reorder them (if requested) 182 | 8) Return 183 | """ 184 | if not self.valves.Server_Address: 185 | raise ValueError("No server address set in valves.") 186 | 187 | # 1) Download the user image 188 | try: 189 | resp = requests.get(image_url, timeout=30) 190 | resp.raise_for_status() 191 | b64_image = base64.b64encode(resp.content).decode("utf-8") 192 | except Exception as e: 193 | return self.valves.error_fetching_image_msg.format(exception=e) 194 | 195 | # Show the original image 196 | if __event_emitter__: 197 | await __event_emitter__( 198 | { 199 | "type": "message", 200 | "data": { 201 | "content": self.valves.original_image_msg.format(url=image_url) 202 | }, 203 | } 204 | ) 205 | 206 | # 2) Possibly fetch remote workflow 207 | if self.valves.workflow_file_url: 208 | try: 209 | w = requests.get(self.valves.workflow_file_url, timeout=30) 210 | w.raise_for_status() 211 | self.workflow_template = w.json() 212 | except Exception as e: 213 | return self.valves.error_retrieving_images_msg.format(exception=e) 214 | 215 | if not self.workflow_template: 216 | return "No workflow loaded; set a .workflow_file_url or define an inline default." 217 | 218 | # 3) Insert placeholders (%%B64IMAGE%%, %%PROMPT%%, etc.) 219 | import copy 220 | 221 | placeholders = {target_node: b64_image} 222 | if prompt_text: 223 | placeholders["%%PROMPT%%"] = prompt_text 224 | 225 | wf_copy = copy.deepcopy(self.workflow_template) 226 | updated_workflow = self._replace_placeholders(wf_copy, placeholders) 227 | 228 | # 4) Build the WS URL 229 | client_id = str(uuid.uuid4()) 230 | ws_url = f"wss://{self.valves.Server_Address}/ws?clientId={client_id}" 231 | if self.valves.Api_Key: 232 | ws_url += f"&token={self.valves.Api_Key}" 233 | 234 | if self.valves.debug_mode: 235 | logging.debug(f"Connecting WebSocket to: {ws_url}") 236 | 237 | if __event_emitter__: 238 | await __event_emitter__( 239 | { 240 | "type": "status", 241 | "data": { 242 | "description": self.valves.connecting_to_comfyui_msg, 243 | "done": False, 244 | }, 245 | } 246 | ) 247 | 248 | prompt_id = None 249 | 250 | # 5) Connect to the WebSocket & submit the workflow 251 | try: 252 | async with websockets.connect(ws_url) as ws: 253 | # Indicate we connected 254 | if __event_emitter__: 255 | await __event_emitter__( 256 | { 257 | "type": "status", 258 | "data": { 259 | "description": self.valves.connected_submitting_workflow_msg, 260 | "done": False, 261 | }, 262 | } 263 | ) 264 | 265 | # Submit the prompt via /prompt 266 | try: 267 | prompt_id = self._queue_prompt(updated_workflow, client_id) 268 | if __event_emitter__: 269 | await __event_emitter__( 270 | { 271 | "type": "status", 272 | "data": { 273 | "description": self.valves.workflow_submitted_msg, 274 | "done": False, 275 | }, 276 | } 277 | ) 278 | except Exception as e: 279 | if __event_emitter__: 280 | await __event_emitter__( 281 | { 282 | "type": "status", 283 | "data": { 284 | "description": self.valves.workflow_submission_failed_msg.format( 285 | exception=e 286 | ), 287 | "done": True, 288 | }, 289 | } 290 | ) 291 | return self.valves.workflow_submission_failed_msg.format( 292 | exception=e 293 | ) 294 | 295 | # 6) indefinite read until node=None or forced close => generation done 296 | generation_done = False 297 | while not generation_done: 298 | try: 299 | raw_msg = await ws.recv() 300 | except websockets.ConnectionClosed: 301 | # forcibly closed => done 302 | break 303 | 304 | if isinstance(raw_msg, str): 305 | # It's JSON 306 | msg_json = json.loads(raw_msg) 307 | msg_type = msg_json.get("type", "") 308 | data = msg_json.get("data", {}) 309 | # typical ComfyUI signals "executing" with "node": None => done 310 | if ( 311 | msg_type == "executing" 312 | and data.get("prompt_id") == prompt_id 313 | and data.get("node") is None 314 | ): 315 | # generation done => break 316 | generation_done = True 317 | # ignoring any binary frames 318 | 319 | except Exception as e: 320 | logging.error(f"WebSocket error: {e}") 321 | if __event_emitter__: 322 | await __event_emitter__( 323 | { 324 | "type": "status", 325 | "data": { 326 | "description": self.valves.ws_connection_error_msg.format( 327 | exception=e 328 | ), 329 | "done": True, 330 | }, 331 | } 332 | ) 333 | return self.valves.ws_connection_error_msg.format(exception=e) 334 | 335 | # 7) Once the server forcibly closed or we saw node=None => fetch /history 336 | if __event_emitter__: 337 | await __event_emitter__( 338 | { 339 | "type": "status", 340 | "data": { 341 | "description": self.valves.comfyui_generation_complete_msg, 342 | "done": False, 343 | }, 344 | } 345 | ) 346 | 347 | if not prompt_id: 348 | return "No prompt_id found; cannot fetch final images." 349 | 350 | hist_url = f"https://{self.valves.Server_Address}/history/{prompt_id}" 351 | if self.valves.Api_Key: 352 | hist_url += f"?token={self.valves.Api_Key}" 353 | 354 | if self.valves.debug_mode: 355 | logging.debug(f"Fetching final images from: {hist_url}") 356 | 357 | try: 358 | r = requests.get(hist_url, timeout=30) 359 | r.raise_for_status() 360 | history_data = r.json() 361 | except Exception as e: 362 | if __event_emitter__: 363 | await __event_emitter__( 364 | { 365 | "type": "status", 366 | "data": { 367 | "description": self.valves.error_retrieving_images_msg.format( 368 | exception=e 369 | ), 370 | "done": True, 371 | }, 372 | } 373 | ) 374 | return self.valves.error_retrieving_images_msg.format(exception=e) 375 | 376 | if prompt_id not in history_data: 377 | if __event_emitter__: 378 | await __event_emitter__( 379 | { 380 | "type": "status", 381 | "data": { 382 | "description": self.valves.no_history_msg, 383 | "done": True, 384 | }, 385 | } 386 | ) 387 | return self.valves.no_history_msg 388 | 389 | outputs = history_data[prompt_id].get("outputs", {}) 390 | if not outputs: 391 | if __event_emitter__: 392 | await __event_emitter__( 393 | { 394 | "type": "status", 395 | "data": { 396 | "description": self.valves.no_images_workflow_msg, 397 | "done": True, 398 | }, 399 | } 400 | ) 401 | return self.valves.no_images_workflow_msg 402 | 403 | # Gather the final images 404 | all_images = [] 405 | for _, node_output in outputs.items(): 406 | node_imgs = node_output.get("images", []) 407 | all_images.extend(node_imgs) 408 | 409 | # Reorder them if user provided a list 410 | order_list_str = self.valves.image_order_list.strip() 411 | if order_list_str: 412 | # Parse the comma-separated 1-based indices 413 | new_order = [] 414 | for part in order_list_str.split(","): 415 | part = part.strip() 416 | if part.isdigit(): 417 | idx = int(part) - 1 # convert to 0-based 418 | if 0 <= idx < len(all_images): 419 | new_order.append(all_images[idx]) 420 | # If new_order is not empty, we replace the entire list 421 | if new_order: 422 | all_images = new_order 423 | 424 | # Now apply max_returned_images 425 | if max_returned_images > 0: 426 | all_images = all_images[:max_returned_images] 427 | 428 | # Emit the final images 429 | image_count = 0 430 | for hist_img in all_images: 431 | fn = hist_img["filename"] 432 | sf = hist_img["subfolder"] 433 | ft = hist_img["type"] 434 | final_url = f"https://{self.valves.Server_Address}/view?filename={fn}&subfolder={sf}&type={ft}" 435 | if self.valves.Api_Key: 436 | final_url += f"&token={self.valves.Api_Key}" 437 | 438 | image_count += 1 439 | if __event_emitter__: 440 | await __event_emitter__( 441 | { 442 | "type": "message", 443 | "data": { 444 | "content": f"**Enhanced Image #{image_count}:** ![Preview]({final_url})" 445 | }, 446 | } 447 | ) 448 | 449 | # final status 450 | if __event_emitter__: 451 | await __event_emitter__( 452 | { 453 | "type": "status", 454 | "data": { 455 | "description": self.valves.workflow_completed_status_msg.format( 456 | image_count=image_count 457 | ), 458 | "done": True, 459 | }, 460 | } 461 | ) 462 | 463 | return self.valves.workflow_completed_return_msg.format(image_count=image_count) 464 | -------------------------------------------------------------------------------- /img2img_sysprompt.txt: -------------------------------------------------------------------------------- 1 | You are **Enhancy**, a friendly, imaginative, and resourceful AI assistant integrated with Open-WebUI and ComfyUI. Your role is to help users refine and improve their existing images by applying advanced enhancements through specialized workflows. You are equipped with a dedicated tool that seamlessly enhances images based on user instructions. 2 | 3 | ### 🛠️ Available Tools 4 | 5 | #### 1. Enhance Image Tool (`img2img_comfy_tool`) 6 | - **Purpose:** Enhance or modify an existing image to improve quality, add variations, or refine details. 7 | - **Usage:** Invoke this tool when a user requests image improvements with phrases like: 8 | - "enhance this image" 9 | - "make variations of this image" 10 | - "refine this image" 11 | - **Parameter:** 12 | - `image_url`: The direct URL of the image to be enhanced. 13 | - **Output:** The enhanced image will be displayed directly in the chat. 14 | 15 | Always ensure that you pass the user's exact image URL to the tool, and maintain a warm, engaging tone in your responses. 16 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # open-webui-tools 2 | 3 | > **NOTE:** The previous *_template.py files are now deprecated and broken due to an update with Open-WebUI. Use the two new files in this repository for Txt2Img and Img2Img functionalities. 4 | 5 | **open-webui-tools** is a repository that provides tools for integrating ComfyUI workflows with Open-WebUI. These tools let you generate images from text prompts (Txt2Img) or enhance existing images (Img2Img) using customizable, JSON-defined ComfyUI workflows. All configuration settings (such as API key, server address, and workflow URL) are now administered via the admin-controlled Valves. 6 | 7 | ## 🛠️ Tools 8 | 9 | - **Txt2Img Tool:** Generate images from text prompts using a customizable ComfyUI workflow. 10 | - **Img2Img Tool:** Enhance or modify existing images with additional instructions. 11 | 12 | ## 📦 Installation 13 | 14 | 1. **Clone the Repository:** 15 | 16 | ```bash 17 | git clone https://github.com/pwillia7/open-webui-tools.git 18 | 19 | ``` 20 | 2. **Navigate to the Repository Directory:** 21 | ```bash 22 | cd open-webui-tools 23 | ``` 24 | ## ⚙️ Setup 25 | ### 1\. Add Tools to Open-WebUI 26 | 27 | 1. **Open Open-WebUI:** 28 | 29 | - Launch Open-WebUI in your web browser. 30 | 2. **Navigate to Workspace:** 31 | 32 | - Click on the **Workspace** tab in the Open-WebUI interface. 33 | 3. **Access Tools:** 34 | 35 | - Select the **Tools** section within the Workspace. 36 | 4. **Create a New Tool:** 37 | 38 | - Click on **Create New Tool**. 39 | - **Name Your Tool:** For example, "Txt2Img Generator" or "Img2Img Enhancer". 40 | - **Paste the Tool Code:** 41 | - Open the corresponding Python file from this repository (e.g., `txt2img.py` or `img2img.py`). 42 | - Copy the entire file content and paste it into the tool creation interface in Open-WebUI. 43 | - **Save the Tool:** Click **Save** to add it to your workspace. 44 | 45 | ### 2\. Customize Workflow Templates 46 | 47 | Both tools contain a `workflow_template` field that is populated by fetching a JSON workflow from a URL. This URL should point to a gist (or another publicly accessible location) that contains your ComfyUI workflow JSON. 48 | 49 | #### Txt2Img Tool 50 | 51 | - **Placeholder:** `%%PROMPT%%` 52 | - **Setup Steps:** 53 | - Create or update a gist that contains your ComfyUI workflow JSON. 54 | - Make sure your workflow JSON includes the `%%PROMPT%%` placeholder where the text prompt will be injected. 55 | - In the tool’s admin settings, set the **Workflow\_URL** to your gist URL. 56 | - [Example Flux Workflow](https://gist.githubusercontent.com/pwillia7/9fe756338c7d35eba130c68408b705f4/raw/4a429e1ede948e02e405e3a046b2eb85546f1c0f/fluxgen) 57 | 58 | #### Img2Img Tool 59 | 60 | - **Placeholders:** `%%B64IMAGE%%` (for the input image) and optionally `%%PROMPT%%` (if additional text instructions are used). 61 | - **Setup Steps:** 62 | - Create or update a gist with your ComfyUI workflow JSON. 63 | - Ensure the JSON includes the `%%B64IMAGE%%` placeholder (and `%%PROMPT%%` if desired). 64 | - In the tool’s admin settings, set the **Workflow\_URL** to point to this gist. 65 | - [Example Enhance! Workflow](https://gist.githubusercontent.com/pwillia7/38bb9fb1da204407339ebe33e66caa35/raw/d0368b32693bb946f9d6a51a7dcec2452138b925/enhance_api_2025.json) 66 | 67 | ### 3\. Configure API Key and Server Address 68 | 69 | Both tools require you to set an API key and the ComfyUI server address. These settings are defined in the admin-only **Valves**. To configure them: 70 | 71 | 1. **Access Tool Settings:** 72 | - In your Open-WebUI workspace, click the gear icon next to the tool. 73 | 2. **Enter Settings:** 74 | - **Api\_Key:** Enter your ComfyUI API key. 75 | - **ComfyUI\_Server:** Enter your ComfyUI server address (without the protocol, e.g., `ptkwilliams.ddns.net:8443`). 76 | - **Workflow\_URL:** Enter the URL of your gist containing the workflow JSON. 77 | - **debug\_mode:** Optionally enable debug mode for extra logging. 78 | 3. **Save Your Settings.** 79 | ## 🚀 Usage 80 | ### Txt2Img Tool 81 | 82 | Invoke the Txt2Img tool with your desired text prompt. The tool replaces the `%%PROMPT%%` placeholder in your JSON workflow with your prompt, submits it to ComfyUI, and displays the generated images within Open-WebUI. 83 | 84 | ### Img2Img Tool 85 | 86 | For image enhancement, provide the URL of the source image along with an optional text prompt. The tool replaces `%%B64IMAGE%%` (and `%%PROMPT%%` if applicable) in your workflow JSON, submits the request, and retrieves the enhanced images. -------------------------------------------------------------------------------- /txt2img_sysprompt.txt: -------------------------------------------------------------------------------- 1 | You are **Genny**, a friendly, imaginative, and resourceful AI assistant integrated with Open-WebUI and ComfyUI. Your mission is to help users bring their creative visions to life by generating or enhancing images based on their descriptions. You are equipped with two specialized tools that enable you to either create entirely new images or modify existing ones using advanced node-based workflows. 2 | 3 | ### 🛠️ Available Tools 4 | 5 | #### 1. Image Generator Tool (`flux_image_maker`) 6 | - **Purpose:** Create completely new images from a text prompt. 7 | - **Usage:** Use this tool when a user requests a new image using phrases like: 8 | - "make an image of..." 9 | - "generate an image of..." 10 | - "create a flux image of..." 11 | - **Parameter:** 12 | - `prompt_text`: The exact description provided by the user for the desired image. 13 | - **Output:** The generated image(s) will be displayed directly in the chat. 14 | 15 | Remember to pass the user's exact text prompt to the tool and maintain a warm, engaging tone in your responses. 16 | --------------------------------------------------------------------------------