├── .gitignore ├── README.md ├── plugins ├── brickbreaker │ ├── app.js │ ├── brickbreaker_capp.html │ ├── brickbreaker_capp.py │ └── style.css ├── forecast │ ├── app.js │ ├── forecast_capp.html │ ├── forecast_capp.py │ └── style.css ├── google_news_feed │ ├── app.js │ ├── google_news_feed_capp.html │ ├── google_news_feed_capp.py │ └── style.css ├── pong │ ├── app.js │ ├── pong_capp.html │ ├── pong_capp.py │ └── style.css ├── snake │ ├── app.js │ ├── snake_capp.html │ ├── snake_capp.py │ └── style.css ├── template │ ├── app.js │ ├── style.css │ ├── template_capp.html │ └── template_capp.py └── tetris │ ├── app.js │ ├── style.css │ ├── tetris_capp.html │ └── tetris_capp.py └── src ├── cerebro.py ├── cerebro_package_manager.json ├── cerebro_tool_launcher.json └── cerebro_tool_launcher.py /.gitignore: -------------------------------------------------------------------------------- 1 | WIP/ 2 | testing.html -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cerebro-OpenWebUI-Package-Manager 2 | 3 | ## Introduction 4 | The **Cerebro-OpenWebUI-Package-Manager** is a comprehensive package management tool designed for the Cerebro OpenWebUI framework. It simplifies the process of managing packages, ensuring seamless integration of GUI applets and additional functionality. 5 | 6 | ## Features 7 | - **Easy Installation and Uninstalling**: Streamline the process of installing and uninstalling packages 8 | - **User-Friendly Interface**: Intuitive and accessible interface for managing packages 9 | - **LLM-Powered Tool Launcher**: Allow LLMs to invoke tools, even without function calling ability (Cerebro Tool Launcher) 10 | 11 | ## Installation 12 | - Ensure you are using OpenWebUI version 0.3.6 or later 13 | - In OpenWebUI navigate to Workspace => Functions => Import Functions 14 | - Import `cerebro_package_manager.json` and `cerebro_tool_launcher.json` from the `src` directory in this repo 15 | - Change the default config options from the gui if needed 16 | - Enable globally or on a per-model basis 17 | 18 | ### Usage 19 | - **List installed packages**: 20 | `owui list` 21 | 22 | - **Install a package**: 23 | `owui install ` 24 | 25 | - **Remove a package**: 26 | `owui uninstall ` 27 | 28 | - **Run a package**: 29 | `owui run ` 30 | 31 | ## Roadmap 32 | - [ ] Package versioning and Update commands 33 | - [ ] Require version numbers 34 | 35 | ## Bugs 36 | - [ ] Uninstalling is not properly removing the actual directory from files - Working on fix 37 | - [ ] `owui list` doesn't work until you have installed at least one package 38 | 39 | ## Contributing 40 | Contributions are welcome! Please follow these steps: 41 | 42 | 1. Fork the repository. 43 | 2. Create a new branch (`git checkout -b feature-branch`). 44 | 3. Make your changes. 45 | 4. Commit your changes (`git commit -am 'Add some feature'`). 46 | 5. Push to the branch (`git push origin feature-branch`). 47 | 6. Create a new Pull Request. 48 | 49 | ## License 50 | 51 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. -------------------------------------------------------------------------------- /plugins/brickbreaker/app.js: -------------------------------------------------------------------------------- 1 | const canvas = document.getElementById('brickbreaker-board'); 2 | const ctx = canvas.getContext('2d'); 3 | const scoreElement = document.getElementById('score'); 4 | const livesElement = document.getElementById('lives'); 5 | const newGameBtn = document.getElementById('new-game-btn'); 6 | 7 | const PADDLE_WIDTH = 100; 8 | const PADDLE_HEIGHT = 10; 9 | const BALL_RADIUS = 8; 10 | const BRICK_ROWS = 5; 11 | const BRICK_COLS = 8; 12 | const BRICK_WIDTH = 60; 13 | const BRICK_HEIGHT = 20; 14 | const BRICK_PADDING = 10; 15 | const BRICK_OFFSET_TOP = 30; 16 | const BRICK_OFFSET_LEFT = 30; 17 | 18 | canvas.width = 600; 19 | canvas.height = 400; 20 | 21 | let paddle = { x: canvas.width / 2 - PADDLE_WIDTH / 2, y: canvas.height - PADDLE_HEIGHT - 10 }; 22 | let ball = { x: canvas.width / 2, y: canvas.height - 30, dx: 4, dy: -4 }; 23 | let bricks = []; 24 | let score = 0; 25 | let lives = 3; 26 | let gameInterval = null; 27 | let gameRunning = false; 28 | 29 | const neonColors = ['#ff00ff', '#00ffff', '#ffff00', '#ff3300', '#33ff00']; 30 | 31 | function createBricks() { 32 | for (let c = 0; c < BRICK_COLS; c++) { 33 | bricks[c] = []; 34 | for (let r = 0; r < BRICK_ROWS; r++) { 35 | bricks[c][r] = { x: 0, y: 0, status: 1, color: neonColors[Math.floor(Math.random() * neonColors.length)] }; 36 | } 37 | } 38 | } 39 | 40 | function drawBricks() { 41 | for (let c = 0; c < BRICK_COLS; c++) { 42 | for (let r = 0; r < BRICK_ROWS; r++) { 43 | if (bricks[c][r].status === 1) { 44 | const brickX = c * (BRICK_WIDTH + BRICK_PADDING) + BRICK_OFFSET_LEFT; 45 | const brickY = r * (BRICK_HEIGHT + BRICK_PADDING) + BRICK_OFFSET_TOP; 46 | bricks[c][r].x = brickX; 47 | bricks[c][r].y = brickY; 48 | ctx.fillStyle = bricks[c][r].color; 49 | ctx.fillRect(brickX, brickY, BRICK_WIDTH, BRICK_HEIGHT); 50 | ctx.shadowBlur = 10; 51 | ctx.shadowColor = bricks[c][r].color; 52 | ctx.strokeStyle = '#fff'; 53 | ctx.strokeRect(brickX, brickY, BRICK_WIDTH, BRICK_HEIGHT); 54 | } 55 | } 56 | } 57 | ctx.shadowBlur = 0; 58 | } 59 | 60 | function drawPaddle() { 61 | ctx.fillStyle = '#00ffff'; 62 | ctx.fillRect(paddle.x, paddle.y, PADDLE_WIDTH, PADDLE_HEIGHT); 63 | ctx.shadowBlur = 10; 64 | ctx.shadowColor = '#00ffff'; 65 | ctx.strokeStyle = '#fff'; 66 | ctx.strokeRect(paddle.x, paddle.y, PADDLE_WIDTH, PADDLE_HEIGHT); 67 | ctx.shadowBlur = 0; 68 | } 69 | 70 | function drawBall() { 71 | ctx.beginPath(); 72 | ctx.arc(ball.x, ball.y, BALL_RADIUS, 0, Math.PI * 2); 73 | ctx.fillStyle = '#ffffff'; 74 | ctx.fill(); 75 | ctx.shadowBlur = 10; 76 | ctx.shadowColor = '#ffffff'; 77 | ctx.strokeStyle = '#00ffff'; 78 | ctx.stroke(); 79 | ctx.closePath(); 80 | ctx.shadowBlur = 0; 81 | } 82 | 83 | function draw() { 84 | ctx.clearRect(0, 0, canvas.width, canvas.height); 85 | drawBricks(); 86 | drawPaddle(); 87 | drawBall(); 88 | } 89 | 90 | function movePaddle(x) { 91 | paddle.x = Math.max(0, Math.min(canvas.width - PADDLE_WIDTH, x - PADDLE_WIDTH / 2)); 92 | } 93 | 94 | function collisionDetection() { 95 | for (let c = 0; c < BRICK_COLS; c++) { 96 | for (let r = 0; r < BRICK_ROWS; r++) { 97 | const b = bricks[c][r]; 98 | if (b.status === 1) { 99 | if (ball.x > b.x && ball.x < b.x + BRICK_WIDTH && ball.y > b.y && ball.y < b.y + BRICK_HEIGHT) { 100 | ball.dy = -ball.dy; 101 | b.status = 0; 102 | score++; 103 | scoreElement.textContent = score; 104 | if (score === BRICK_ROWS * BRICK_COLS) { 105 | alert('Congratulations! You win!'); 106 | stopGame(); 107 | } 108 | } 109 | } 110 | } 111 | } 112 | } 113 | 114 | function updateBall() { 115 | ball.x += ball.dx; 116 | ball.y += ball.dy; 117 | 118 | // Wall collision 119 | if (ball.x - BALL_RADIUS < 0 || ball.x + BALL_RADIUS > canvas.width) { 120 | ball.dx = -ball.dx; 121 | } 122 | if (ball.y - BALL_RADIUS < 0) { 123 | ball.dy = -ball.dy; 124 | } 125 | 126 | // Paddle collision 127 | if (ball.y + BALL_RADIUS > paddle.y && ball.y - BALL_RADIUS < paddle.y + PADDLE_HEIGHT && 128 | ball.x > paddle.x && ball.x < paddle.x + PADDLE_WIDTH) { 129 | let hitPos = (ball.x - paddle.x) / PADDLE_WIDTH; 130 | ball.dx = 8 * (hitPos - 0.5); 131 | ball.dy = -Math.abs(ball.dy); // Ensure the ball always goes up after hitting the paddle 132 | ball.y = paddle.y - BALL_RADIUS; // Adjust ball position to prevent it from going through the paddle 133 | } 134 | 135 | // Ball out of bounds 136 | if (ball.y + BALL_RADIUS > canvas.height) { 137 | lives--; 138 | livesElement.textContent = lives; 139 | if (lives === 0) { 140 | alert('Game Over'); 141 | stopGame(); 142 | } else { 143 | ball.x = canvas.width / 2; 144 | ball.y = canvas.height - 30; 145 | ball.dx = 4; 146 | ball.dy = -4; 147 | paddle.x = (canvas.width - PADDLE_WIDTH) / 2; 148 | } 149 | } 150 | } 151 | 152 | function gameLoop() { 153 | draw(); 154 | updateBall(); 155 | collisionDetection(); 156 | } 157 | 158 | function stopGame() { 159 | clearInterval(gameInterval); 160 | gameRunning = false; 161 | newGameBtn.textContent = "New Game"; 162 | } 163 | 164 | function startGame() { 165 | if (gameRunning) { 166 | stopGame(); 167 | } 168 | resetGame(); 169 | gameRunning = true; 170 | gameInterval = setInterval(gameLoop, 1000 / 60); 171 | newGameBtn.textContent = "Restart Game"; 172 | } 173 | 174 | function resetGame() { 175 | stopGame(); 176 | paddle = { x: canvas.width / 2 - PADDLE_WIDTH / 2, y: canvas.height - PADDLE_HEIGHT - 10 }; 177 | ball = { x: canvas.width / 2, y: canvas.height - 30, dx: 4, dy: -4 }; 178 | createBricks(); 179 | score = 0; 180 | lives = 3; 181 | scoreElement.textContent = score; 182 | livesElement.textContent = lives; 183 | } 184 | 185 | function handleMouseMove(event) { 186 | if (!gameRunning) return; 187 | const rect = canvas.getBoundingClientRect(); 188 | const mouseX = event.clientX - rect.left; 189 | movePaddle(mouseX); 190 | } 191 | 192 | canvas.addEventListener('mousemove', handleMouseMove); 193 | newGameBtn.addEventListener('click', startGame); 194 | 195 | canvas.addEventListener('mouseenter', () => { 196 | canvas.style.cursor = 'none'; 197 | }); 198 | 199 | canvas.addEventListener('mouseleave', () => { 200 | canvas.style.cursor = 'default'; 201 | }); 202 | 203 | createBricks(); 204 | draw(); -------------------------------------------------------------------------------- /plugins/brickbreaker/brickbreaker_capp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Neon Brick Breaker 9 | 10 | 11 | 12 |
13 |
14 | Score: 0 | Lives: 3 15 |
16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /plugins/brickbreaker/brickbreaker_capp.py: -------------------------------------------------------------------------------- 1 | """ 2 | title: Brickbreaker 3 | author: Andrew Tait Gehrhardt 4 | author_url: https://github.com/atgehrhardt/Cerebro-OpenWebUI-Package-Manager/plugins/brickbreaker 5 | funding_url: https://github.com/open-webui 6 | version: 0.1.0 7 | """ 8 | 9 | import asyncio 10 | from asyncio import sleep 11 | from pydantic import BaseModel, Field 12 | from typing import Optional, Union, Generator, Iterator 13 | from apps.webui.models.files import Files 14 | from config import UPLOAD_DIR 15 | 16 | 17 | class Tools: 18 | """ 19 | Launches a game of Brickbreaker 20 | """ 21 | 22 | class Valves(BaseModel): 23 | priority: int = Field( 24 | default=0, description="Priority level for the filter operations." 25 | ) 26 | 27 | def __init__(self): 28 | self.valves = self.Valves() 29 | self.package_name = "brickbreaker" 30 | self.applet_file_id = None 31 | 32 | async def run( 33 | self, 34 | body: Optional[dict] = None, 35 | __user__: Optional[dict] = None, 36 | __event_emitter__: Optional[callable] = None, 37 | __event_call__: Optional[callable] = None, 38 | ) -> str: 39 | """ 40 | Retrieves information about the weather 41 | :param body: The request body. 42 | :param __user__: User information, including the user ID. 43 | :param __event_emitter__: Function to emit events during the process. 44 | :param __event_call__: Function to call for the final output. 45 | :return: The final message or an empty string. 46 | """ 47 | if not __user__ or "id" not in __user__: 48 | return "Error: User ID not provided" 49 | if not __event_emitter__ or not __event_call__: 50 | return "Error: Event emitter or event call not provided" 51 | 52 | user_id = __user__["id"] 53 | 54 | try: 55 | expected_filename = f"{UPLOAD_DIR}/cerebro/plugins/{self.package_name}/{self.package_name}_capp.html" 56 | all_files = Files.get_files() 57 | matching_file = next( 58 | ( 59 | file 60 | for file in all_files 61 | if file.user_id == user_id and file.filename == expected_filename 62 | ), 63 | None, 64 | ) 65 | 66 | if not matching_file: 67 | error_message = f"Error: Applet file for {self.package_name} not found. Make sure the package is installed." 68 | await __event_emitter__( 69 | {"type": "replace", "data": {"content": error_message}} 70 | ) 71 | await __event_call__(error_message) 72 | return error_message 73 | 74 | self.applet_file_id = matching_file.id 75 | 76 | # Simulate a loading process 77 | loading_messages = [ 78 | "Applet file found... launching", 79 | ] 80 | for message in loading_messages: 81 | await __event_emitter__( 82 | {"type": "replace", "data": {"content": message}} 83 | ) 84 | await asyncio.sleep(1) 85 | 86 | # Finally, replace with the actual applet embed 87 | final_message = f"{{{{HTML_FILE_ID_{self.applet_file_id}}}}}" 88 | await __event_emitter__( 89 | {"type": "replace", "data": {"content": final_message}} 90 | ) 91 | 92 | # Simulate a short delay to ensure the message is displayed 93 | await sleep(0.5) 94 | 95 | return "" 96 | 97 | except Exception as e: 98 | error_message = f"An error occurred while launching the applet: {str(e)}" 99 | await __event_emitter__( 100 | {"type": "replace", "data": {"content": error_message}} 101 | ) 102 | await __event_call__(error_message) 103 | return error_message -------------------------------------------------------------------------------- /plugins/brickbreaker/style.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin: 0; 3 | padding: 0; 4 | overflow: hidden; 5 | color: #fff; 6 | font-family: 'Arial', sans-serif; 7 | width: 100%; 8 | height: 100%; 9 | background-color: transparent; 10 | } 11 | #game-container { 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | width: 100%; 16 | padding-top: 10px; 17 | } 18 | #brickbreaker-board { 19 | border: 4px solid #00ffff; 20 | cursor: none; 21 | box-shadow: 0 0 20px #00ffff; 22 | } 23 | #info { 24 | background-color: rgba(0, 255, 255, 0.2); 25 | padding: 5px 10px; 26 | border-radius: 5px; 27 | margin-bottom: 5px; 28 | font-weight: bold; 29 | text-shadow: 0 0 5px #00ffff; 30 | } 31 | #new-game-btn { 32 | margin-top: 10px; 33 | padding: 5px 10px; 34 | background-color: #ff00ff; 35 | color: #fff; 36 | border: none; 37 | border-radius: 5px; 38 | cursor: pointer; 39 | font-weight: bold; 40 | text-shadow: 0 0 5px #ff00ff; 41 | box-shadow: 0 0 10px #ff00ff; 42 | } 43 | #new-game-btn:hover { 44 | background-color: #ff60ff; 45 | } -------------------------------------------------------------------------------- /plugins/forecast/app.js: -------------------------------------------------------------------------------- 1 | async function getLocationAndWeather() { 2 | if ("geolocation" in navigator) { 3 | navigator.geolocation.getCurrentPosition(async function(position) { 4 | const lat = position.coords.latitude; 5 | const lon = position.coords.longitude; 6 | await getWeather(lat, lon); 7 | }, function(error) { 8 | console.error("Error getting location:", error); 9 | document.getElementById('weather-info').textContent = 'Error getting location. Please enable location services.'; 10 | }); 11 | } else { 12 | document.getElementById('weather-info').textContent = 'Geolocation is not supported by your browser.'; 13 | } 14 | } 15 | 16 | async function getWeather(lat, lon) { 17 | try { 18 | const headers = { 19 | "User-Agent": "(myweatherapp.com, contact@myweatherapp.com)", 20 | "Accept": "application/geo+json" 21 | }; 22 | 23 | // First, get the forecast URL for the location 24 | const pointsResponse = await fetch(`https://api.weather.gov/points/${lat},${lon}`, { headers }); 25 | if (!pointsResponse.ok) { 26 | throw new Error(`HTTP error! status: ${pointsResponse.status}`); 27 | } 28 | const pointsData = await pointsResponse.json(); 29 | 30 | // Now, get the actual forecast 31 | const forecastResponse = await fetch(pointsData.properties.forecast, { headers }); 32 | if (!forecastResponse.ok) { 33 | throw new Error(`HTTP error! status: ${forecastResponse.status}`); 34 | } 35 | const forecastData = await forecastResponse.json(); 36 | 37 | // Extract relevant information 38 | const currentPeriod = forecastData.properties.periods[0]; 39 | const temperature = currentPeriod.temperature; 40 | const temperatureUnit = currentPeriod.temperatureUnit; 41 | const description = currentPeriod.shortForecast; 42 | const windSpeed = currentPeriod.windSpeed; 43 | const windDirection = currentPeriod.windDirection; 44 | const location = pointsData.properties.relativeLocation.properties.city + ', ' + 45 | pointsData.properties.relativeLocation.properties.state; 46 | 47 | const weatherInfo = document.getElementById('weather-info'); 48 | weatherInfo.innerHTML = ` 49 |
${location}
50 |
${description}
51 |
${temperature}°${temperatureUnit}
52 |
${windSpeed} from ${windDirection}
53 | `; 54 | } catch (error) { 55 | console.error('Error fetching weather data:', error); 56 | document.getElementById('weather-info').textContent = 'Error fetching weather data'; 57 | } 58 | } 59 | 60 | getLocationAndWeather(); 61 | 62 | function consumeEvent(event) { 63 | event.preventDefault(); 64 | event.stopPropagation(); 65 | console.log('Key captured: ' + event.key); 66 | } 67 | 68 | window.addEventListener('keydown', consumeEvent, true); 69 | window.addEventListener('keyup', consumeEvent, true); -------------------------------------------------------------------------------- /plugins/forecast/forecast_capp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Weather Card 9 | 10 | 11 | 12 | 13 |
14 |

Current Weather

15 |
16 | Loading weather data... 17 |
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /plugins/forecast/forecast_capp.py: -------------------------------------------------------------------------------- 1 | """ 2 | title: Forecast 3 | author: Andrew Tait Gehrhardt 4 | author_url: https://github.com/atgehrhardt/Cerebro-OpenWebUI-Package-Manager/plugins/forecast 5 | funding_url: https://github.com/open-webui 6 | version: 0.1.0 7 | """ 8 | 9 | import asyncio 10 | from asyncio import sleep 11 | from pydantic import BaseModel, Field 12 | from typing import Optional 13 | from apps.webui.models.files import Files 14 | from config import UPLOAD_DIR 15 | import aiohttp 16 | 17 | 18 | class Tools: 19 | """ 20 | Used to retrieve forecast data based on user's detailed location 21 | """ 22 | 23 | class Valves(BaseModel): 24 | priority: int = Field( 25 | default=0, description="Priority level for the filter operations." 26 | ) 27 | 28 | def __init__(self): 29 | self.valves = self.Valves() 30 | self.package_name = "forecast" 31 | self.applet_file_id = None 32 | 33 | async def get_user_location(self, session): 34 | """ 35 | Get user's detailed location based on IP address 36 | """ 37 | async with session.get("https://ipapi.co/json/") as response: 38 | if response.status != 200: 39 | raise Exception(f"Location API returned status code {response.status}") 40 | location_data = await response.json() 41 | return { 42 | "latitude": location_data["latitude"], 43 | "longitude": location_data["longitude"], 44 | "city": location_data["city"], 45 | "region": location_data["region"], 46 | "country": location_data["country_name"], 47 | "postal": location_data["postal"], 48 | } 49 | 50 | async def run( 51 | self, 52 | body: Optional[dict] = None, 53 | __user__: Optional[dict] = None, 54 | __event_emitter__: Optional[callable] = None, 55 | __event_call__: Optional[callable] = None, 56 | ) -> str: 57 | """ 58 | Retrieves information about the weather for the user's detailed location 59 | :param body: The request body. 60 | :param __user__: User information, including the user ID. 61 | :param __event_emitter__: Function to emit events during the process. 62 | :param __event_call__: Function to call for the final output. 63 | :return: The final message or an empty string. 64 | """ 65 | if not __user__ or "id" not in __user__: 66 | return "Error: User ID not provided" 67 | if not __event_emitter__ or not __event_call__: 68 | return "Error: Event emitter or event call not provided" 69 | 70 | user_id = __user__["id"] 71 | 72 | try: 73 | expected_filename = f"{UPLOAD_DIR}/cerebro/plugins/{self.package_name}/{self.package_name}_capp.html" 74 | all_files = Files.get_files() 75 | matching_file = next( 76 | ( 77 | file 78 | for file in all_files 79 | if file.user_id == user_id and file.filename == expected_filename 80 | ), 81 | None, 82 | ) 83 | 84 | if not matching_file: 85 | error_message = f"Error: Applet file for {self.package_name} not found. Make sure the package is installed." 86 | await __event_emitter__( 87 | {"type": "replace", "data": {"content": error_message}} 88 | ) 89 | await __event_call__(error_message) 90 | return error_message 91 | 92 | self.applet_file_id = matching_file.id 93 | 94 | # Simulate a loading process 95 | loading_messages = [ 96 | "Fetching weather data...", 97 | ] 98 | for message in loading_messages: 99 | await __event_emitter__( 100 | {"type": "replace", "data": {"content": message}} 101 | ) 102 | await asyncio.sleep(1) 103 | 104 | headers = { 105 | "User-Agent": "(myweatherapp.com, contact@myweatherapp.com)", 106 | "Accept": "application/geo+json", 107 | } 108 | 109 | async with aiohttp.ClientSession() as session: 110 | # Get user's detailed location 111 | location = await self.get_user_location(session) 112 | 113 | # Get the forecast URL for the location 114 | points_url = f"https://api.weather.gov/points/{location['latitude']},{location['longitude']}" 115 | async with session.get(points_url, headers=headers) as response: 116 | if response.status != 200: 117 | raise Exception(f"API returned status code {response.status}") 118 | points_data = await response.json() 119 | 120 | # Get the actual forecast 121 | forecast_url = points_data["properties"]["forecast"] 122 | async with session.get(forecast_url, headers=headers) as response: 123 | if response.status != 200: 124 | raise Exception(f"API returned status code {response.status}") 125 | forecast_data = await response.json() 126 | 127 | # Extract relevant information 128 | current_period = forecast_data["properties"]["periods"][0] 129 | temperature = current_period["temperature"] 130 | temperature_unit = current_period["temperatureUnit"] 131 | description = current_period["shortForecast"] 132 | wind_speed = current_period["windSpeed"] 133 | wind_direction = current_period["windDirection"] 134 | 135 | # Prepare the detailed location string 136 | detailed_location = ( 137 | f"{location['city']}, {location['region']}, {location['country']}" 138 | ) 139 | 140 | # Prepare the weather report 141 | weather_report = f"Current weather in {detailed_location}:\n" 142 | weather_report += f"Temperature: {temperature}°{temperature_unit}\n" 143 | weather_report += f"Description: {description}\n" 144 | weather_report += f"Wind: {wind_speed} from {wind_direction}" 145 | 146 | # Finally, replace with the actual applet embed and weather report 147 | final_message = f"""{{{{HTML_FILE_ID_{self.applet_file_id}}}}} 148 | """ 149 | await __event_emitter__( 150 | {"type": "replace", "data": {"content": final_message}} 151 | ) 152 | 153 | # Simulate a short delay to ensure the message is displayed 154 | await sleep(0.5) 155 | 156 | return f"""You can find a summary of the weather for {detailed_location} below:\n\n 157 | {weather_report} 158 | 159 | Please give a detailed summary of the weather report below and ensure you infor the user of the location. 160 | \n\n\n 161 | """ 162 | 163 | except Exception as e: 164 | error_message = ( 165 | f"An error occurred while fetching the weather data: {str(e)}" 166 | ) 167 | print(f"Debug - Error details: {e}") 168 | await __event_emitter__( 169 | {"type": "replace", "data": {"content": error_message}} 170 | ) 171 | await __event_call__(error_message) 172 | return error_message 173 | -------------------------------------------------------------------------------- /plugins/forecast/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: transparent; 3 | margin: 0; 4 | padding: 0; 5 | display: flex; 6 | justify-content: flex-start; 7 | align-items: center; 8 | min-height: 100vh; 9 | } 10 | 11 | .card { 12 | background-color: #2c3e50; 13 | border-radius: 15px; 14 | padding: 15px; 15 | width: 250px; 16 | margin-left: 20px; 17 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 18 | color: white; 19 | font-family: Arial, sans-serif; 20 | } 21 | 22 | .card h2 { 23 | margin-top: 0; 24 | margin-bottom: 15px; 25 | font-size: 18px; 26 | text-align: center; 27 | } 28 | 29 | .weather-info { 30 | display: grid; 31 | grid-template-columns: repeat(2, 1fr); 32 | gap: 10px; 33 | font-size: 14px; 34 | } 35 | 36 | .weather-item { 37 | display: flex; 38 | align-items: center; 39 | } 40 | 41 | .weather-item i { 42 | margin-right: 5px; 43 | width: 20px; 44 | text-align: center; 45 | } -------------------------------------------------------------------------------- /plugins/google_news_feed/app.js: -------------------------------------------------------------------------------- 1 | const RSS_URL = "https://news.google.com/rss?hl=en-US&gl=US&ceid=US:en"; 2 | const API_URL = `https://api.rss2json.com/v1/api.json?rss_url=${encodeURIComponent( 3 | RSS_URL 4 | )}`; 5 | 6 | function fetchNews() { 7 | const newsListElement = document.getElementById("news-list"); 8 | newsListElement.innerHTML = '
Fetching news...
'; 9 | 10 | fetch(API_URL) 11 | .then((response) => { 12 | if (!response.ok) { 13 | throw new Error(`HTTP error! status: ${response.status}`); 14 | } 15 | return response.json(); 16 | }) 17 | .then((data) => { 18 | if (data.status !== "ok") { 19 | throw new Error("Error fetching RSS feed"); 20 | } 21 | if (data.items.length === 0) { 22 | throw new Error("No news items found in the feed"); 23 | } 24 | let html = '
'; 25 | data.items.forEach((item) => { 26 | html += ` 27 | 32 | `; 33 | }); 34 | html += "
"; 35 | newsListElement.innerHTML = html; 36 | }) 37 | .catch((error) => { 38 | console.error("Error fetching news:", error); 39 | newsListElement.innerHTML = `

Error loading news: ${error.message}. Please try again later.

40 |

Technical details: ${error.stack}

`; 41 | }); 42 | } 43 | 44 | fetchNews(); 45 | setInterval(fetchNews, 300000); // Refresh every 5 minutes 46 | function consumeEvent(event) { 47 | event.preventDefault(); 48 | event.stopPropagation(); 49 | console.log("Key captured: " + event.key); 50 | } 51 | 52 | window.addEventListener("keydown", consumeEvent, true); 53 | window.addEventListener("keyup", consumeEvent, true); -------------------------------------------------------------------------------- /plugins/google_news_feed/google_news_feed_capp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Google News RSS Feed 9 | 10 | 11 | 12 |
13 |

Google News

14 |
15 |
Loading news...
16 |
17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /plugins/google_news_feed/google_news_feed_capp.py: -------------------------------------------------------------------------------- 1 | """ 2 | title: Google News Feed 3 | author: Andrew Tait Gehrhardt 4 | author_url: https://github.com/atgehrhardt/Cerebro-OpenWebUI-Package-Manager/plugins/google_news_feed 5 | funding_url: https://github.com/open-webui 6 | version: 0.1.0 7 | """ 8 | 9 | import asyncio 10 | from asyncio import sleep 11 | from pydantic import BaseModel, Field 12 | from typing import Optional, Union, Generator, Iterator 13 | from apps.webui.models.files import Files 14 | from config import UPLOAD_DIR 15 | 16 | 17 | class Tools: 18 | """ 19 | Tool to retrieve the current and recent news. This can be used to show news to the user. 20 | """ 21 | 22 | class Valves(BaseModel): 23 | priority: int = Field( 24 | default=0, description="Priority level for the filter operations." 25 | ) 26 | 27 | def __init__(self): 28 | self.valves = self.Valves() 29 | self.package_name = "google_news_feed" 30 | self.applet_file_id = None 31 | 32 | async def run( 33 | self, 34 | body: Optional[dict] = None, 35 | __user__: Optional[dict] = None, 36 | __event_emitter__: Optional[callable] = None, 37 | __event_call__: Optional[callable] = None, 38 | ) -> str: 39 | """ 40 | Retrieves information about the weather 41 | :param body: The request body. 42 | :param __user__: User information, including the user ID. 43 | :param __event_emitter__: Function to emit events during the process. 44 | :param __event_call__: Function to call for the final output. 45 | :return: The final message or an empty string. 46 | """ 47 | if not __user__ or "id" not in __user__: 48 | return "Error: User ID not provided" 49 | if not __event_emitter__ or not __event_call__: 50 | return "Error: Event emitter or event call not provided" 51 | 52 | user_id = __user__["id"] 53 | 54 | try: 55 | expected_filename = f"{UPLOAD_DIR}/cerebro/plugins/{self.package_name}/{self.package_name}_capp.html" 56 | all_files = Files.get_files() 57 | matching_file = next( 58 | ( 59 | file 60 | for file in all_files 61 | if file.user_id == user_id and file.filename == expected_filename 62 | ), 63 | None, 64 | ) 65 | 66 | if not matching_file: 67 | error_message = f"Error: Applet file for {self.package_name} not found. Make sure the package is installed." 68 | await __event_emitter__( 69 | {"type": "replace", "data": {"content": error_message}} 70 | ) 71 | await __event_call__(error_message) 72 | return error_message 73 | 74 | self.applet_file_id = matching_file.id 75 | 76 | # Simulate a loading process 77 | loading_messages = [ 78 | "Applet file found... launching", 79 | ] 80 | for message in loading_messages: 81 | await __event_emitter__( 82 | {"type": "replace", "data": {"content": message}} 83 | ) 84 | await asyncio.sleep(1) 85 | 86 | # Finally, replace with the actual applet embed 87 | final_message = f"{{{{HTML_FILE_ID_{self.applet_file_id}}}}}" 88 | await __event_emitter__( 89 | {"type": "replace", "data": {"content": final_message}} 90 | ) 91 | 92 | # Simulate a short delay to ensure the message is displayed 93 | await sleep(0.5) 94 | 95 | return "Inform the user that you have retrieved the news for them and they can view it in the window above." 96 | 97 | except Exception as e: 98 | error_message = f"An error occurred while launching the applet: {str(e)}" 99 | await __event_emitter__( 100 | {"type": "replace", "data": {"content": error_message}} 101 | ) 102 | await __event_call__(error_message) 103 | return error_message 104 | -------------------------------------------------------------------------------- /plugins/google_news_feed/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: transparent; 3 | margin: 0; 4 | padding: 0; 5 | display: flex; 6 | justify-content: flex-start; 7 | align-items: flex-start; 8 | min-height: 350vh; 9 | font-family: 'Roboto', 'Segoe UI', 'Arial', sans-serif; 10 | overflow: hidden; 11 | } 12 | 13 | .card { 14 | background-color: rgba(44, 62, 80, 0.6); 15 | border-radius: 20px; 16 | padding: 25px; 17 | width: 900px; 18 | max-height: 80vh; 19 | margin-left: 20px; 20 | margin-top: 20px; 21 | box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15); 22 | color: white; 23 | overflow-y: auto; 24 | backdrop-filter: blur(15px); 25 | } 26 | 27 | .card h2 { 28 | margin-top: 0; 29 | border-bottom: 2px solid rgba(52, 73, 94, 0.5); 30 | padding-bottom: 15px; 31 | font-size: 28px; 32 | color: #ecf0f1; 33 | letter-spacing: 1px; 34 | text-transform: uppercase; 35 | } 36 | 37 | .news-grid { 38 | display: grid; 39 | grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); 40 | gap: 25px; 41 | padding: 25px 0; 42 | } 43 | 44 | .news-item { 45 | background-color: rgba(52, 73, 94, 0.5); 46 | border-radius: 15px; 47 | padding: 20px; 48 | transition: all 0.3s ease; 49 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); 50 | } 51 | 52 | .news-item:hover { 53 | transform: translateY(-5px); 54 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); 55 | background-color: rgba(52, 73, 94, 0.7); 56 | } 57 | 58 | .news-item a { 59 | color: #3498db; 60 | text-decoration: none; 61 | font-size: 17px; 62 | line-height: 1.5; 63 | display: block; 64 | font-weight: 500; 65 | transition: color 0.3s ease; 66 | } 67 | 68 | .news-item a:hover { 69 | color: #2ecc71; 70 | } 71 | 72 | #loading { 73 | text-align: center; 74 | font-size: 20px; 75 | color: #ecf0f1; 76 | padding: 25px; 77 | font-weight: 300; 78 | letter-spacing: 1px; 79 | } -------------------------------------------------------------------------------- /plugins/pong/app.js: -------------------------------------------------------------------------------- 1 | const canvas = document.getElementById('pong-board'); 2 | const ctx = canvas.getContext('2d'); 3 | const playerScoreElement = document.getElementById('player-score'); 4 | const aiScoreElement = document.getElementById('ai-score'); 5 | const newGameBtn = document.getElementById('new-game-btn'); 6 | 7 | const PADDLE_WIDTH = 10; 8 | const PADDLE_HEIGHT = 80; 9 | const BALL_SIZE = 10; 10 | const BALL_SPEED = 6; 11 | const AI_SPEED = 3; 12 | 13 | canvas.width = 600; 14 | canvas.height = 400; 15 | 16 | let playerPaddle = { x: 10, y: canvas.height / 2 - PADDLE_HEIGHT / 2 }; 17 | let aiPaddle = { x: canvas.width - PADDLE_WIDTH - 10, y: canvas.height / 2 - PADDLE_HEIGHT / 2 }; 18 | let ball = { x: canvas.width / 2, y: canvas.height / 2, dx: BALL_SPEED, dy: BALL_SPEED }; 19 | let playerScore = 0; 20 | let aiScore = 0; 21 | let gameInterval = null; 22 | let gameRunning = false; 23 | 24 | function drawRect(x, y, width, height, color) { 25 | ctx.fillStyle = color; 26 | ctx.fillRect(x, y, width, height); 27 | } 28 | 29 | function drawCircle(x, y, radius, color) { 30 | ctx.fillStyle = color; 31 | ctx.beginPath(); 32 | ctx.arc(x, y, radius, 0, Math.PI * 2, false); 33 | ctx.fill(); 34 | } 35 | 36 | function drawNet() { 37 | for (let i = 0; i <= canvas.height; i += 20) { 38 | drawRect(canvas.width / 2 - 1, i, 2, 15, '#ecf0f1'); 39 | } 40 | } 41 | 42 | function draw() { 43 | drawRect(0, 0, canvas.width, canvas.height, '#000'); 44 | drawNet(); 45 | drawRect(playerPaddle.x, playerPaddle.y, PADDLE_WIDTH, PADDLE_HEIGHT, '#ecf0f1'); 46 | drawRect(aiPaddle.x, aiPaddle.y, PADDLE_WIDTH, PADDLE_HEIGHT, '#ecf0f1'); 47 | drawCircle(ball.x, ball.y, BALL_SIZE / 2, '#ecf0f1'); 48 | } 49 | 50 | function movePaddle(paddle, y) { 51 | paddle.y = Math.max(0, Math.min(canvas.height - PADDLE_HEIGHT, y)); 52 | } 53 | 54 | function updateAI() { 55 | const paddleCenter = aiPaddle.y + PADDLE_HEIGHT / 2; 56 | if (paddleCenter < ball.y - 35) { 57 | aiPaddle.y += AI_SPEED; 58 | } else if (paddleCenter > ball.y + 35) { 59 | aiPaddle.y -= AI_SPEED; 60 | } 61 | } 62 | 63 | function updateBall() { 64 | ball.x += ball.dx; 65 | ball.y += ball.dy; 66 | 67 | if (ball.y - BALL_SIZE / 2 < 0 || ball.y + BALL_SIZE / 2 > canvas.height) { 68 | ball.dy *= -1; 69 | } 70 | 71 | if ( 72 | (ball.dx < 0 && ball.x - BALL_SIZE / 2 < playerPaddle.x + PADDLE_WIDTH && ball.y > playerPaddle.y && ball.y < playerPaddle.y + PADDLE_HEIGHT) || 73 | (ball.dx > 0 && ball.x + BALL_SIZE / 2 > aiPaddle.x && ball.y > aiPaddle.y && ball.y < aiPaddle.y + PADDLE_HEIGHT) 74 | ) { 75 | ball.dx *= -1; 76 | const paddleCenter = (ball.dx < 0 ? playerPaddle.y : aiPaddle.y) + PADDLE_HEIGHT / 2; 77 | const relativeIntersectY = (ball.y - paddleCenter) / (PADDLE_HEIGHT / 2); 78 | ball.dy = BALL_SPEED * relativeIntersectY; 79 | } 80 | 81 | if (ball.x - BALL_SIZE / 2 < 0) { 82 | aiScore++; 83 | aiScoreElement.textContent = aiScore; 84 | resetBall(); 85 | } else if (ball.x + BALL_SIZE / 2 > canvas.width) { 86 | playerScore++; 87 | playerScoreElement.textContent = playerScore; 88 | resetBall(); 89 | } 90 | } 91 | 92 | function resetBall() { 93 | ball.x = canvas.width / 2; 94 | ball.y = canvas.height / 2; 95 | ball.dx = -ball.dx; 96 | ball.dy = Math.random() * BALL_SPEED * 2 - BALL_SPEED; 97 | } 98 | 99 | function gameLoop() { 100 | updateAI(); 101 | updateBall(); 102 | draw(); 103 | } 104 | 105 | function stopGame() { 106 | clearInterval(gameInterval); 107 | gameRunning = false; 108 | newGameBtn.textContent = "New Game"; 109 | } 110 | 111 | function startGame() { 112 | if (gameRunning) { 113 | stopGame(); 114 | } 115 | resetGame(); 116 | gameRunning = true; 117 | gameInterval = setInterval(gameLoop, 1000 / 60); 118 | newGameBtn.textContent = "Restart Game"; 119 | } 120 | 121 | function resetGame() { 122 | stopGame(); 123 | playerPaddle = { x: 10, y: canvas.height / 2 - PADDLE_HEIGHT / 2 }; 124 | aiPaddle = { x: canvas.width - PADDLE_WIDTH - 10, y: canvas.height / 2 - PADDLE_HEIGHT / 2 }; 125 | ball = { x: canvas.width / 2, y: canvas.height / 2, dx: BALL_SPEED, dy: BALL_SPEED }; 126 | playerScore = 0; 127 | aiScore = 0; 128 | playerScoreElement.textContent = playerScore; 129 | aiScoreElement.textContent = aiScore; 130 | } 131 | 132 | function handleMouseMove(event) { 133 | if (!gameRunning) return; 134 | const rect = canvas.getBoundingClientRect(); 135 | const mouseY = event.clientY - rect.top; 136 | movePaddle(playerPaddle, mouseY - PADDLE_HEIGHT / 2); 137 | } 138 | 139 | canvas.addEventListener('mousemove', handleMouseMove); 140 | newGameBtn.addEventListener('click', startGame); 141 | 142 | canvas.addEventListener('mouseenter', () => { 143 | canvas.style.cursor = 'none'; 144 | }); 145 | 146 | canvas.addEventListener('mouseleave', () => { 147 | canvas.style.cursor = 'default'; 148 | }); 149 | 150 | draw(); -------------------------------------------------------------------------------- /plugins/pong/pong_capp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Pong 9 | 10 | 11 | 12 |
13 |
14 | Player: 0 | AI: 0 15 |
16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /plugins/pong/pong_capp.py: -------------------------------------------------------------------------------- 1 | """ 2 | title: Pong 3 | author: Andrew Tait Gehrhardt 4 | author_url: https://github.com/atgehrhardt/Cerebro-OpenWebUI-Package-Manager/plugins/pong 5 | funding_url: https://github.com/open-webui 6 | version: 0.1.0 7 | """ 8 | 9 | import asyncio 10 | from asyncio import sleep 11 | from pydantic import BaseModel, Field 12 | from typing import Optional, Union, Generator, Iterator 13 | from apps.webui.models.files import Files 14 | from config import UPLOAD_DIR 15 | 16 | 17 | class Tools: 18 | """ 19 | Launches a game of Pong 20 | """ 21 | 22 | class Valves(BaseModel): 23 | priority: int = Field( 24 | default=0, description="Priority level for the filter operations." 25 | ) 26 | 27 | def __init__(self): 28 | self.valves = self.Valves() 29 | self.package_name = "pong" 30 | self.applet_file_id = None 31 | 32 | async def run( 33 | self, 34 | body: Optional[dict] = None, 35 | __user__: Optional[dict] = None, 36 | __event_emitter__: Optional[callable] = None, 37 | __event_call__: Optional[callable] = None, 38 | ) -> str: 39 | """ 40 | Retrieves information about the weather 41 | :param body: The request body. 42 | :param __user__: User information, including the user ID. 43 | :param __event_emitter__: Function to emit events during the process. 44 | :param __event_call__: Function to call for the final output. 45 | :return: The final message or an empty string. 46 | """ 47 | if not __user__ or "id" not in __user__: 48 | return "Error: User ID not provided" 49 | if not __event_emitter__ or not __event_call__: 50 | return "Error: Event emitter or event call not provided" 51 | 52 | user_id = __user__["id"] 53 | 54 | try: 55 | expected_filename = f"{UPLOAD_DIR}/cerebro/plugins/{self.package_name}/{self.package_name}_capp.html" 56 | all_files = Files.get_files() 57 | matching_file = next( 58 | ( 59 | file 60 | for file in all_files 61 | if file.user_id == user_id and file.filename == expected_filename 62 | ), 63 | None, 64 | ) 65 | 66 | if not matching_file: 67 | error_message = f"Error: Applet file for {self.package_name} not found. Make sure the package is installed." 68 | await __event_emitter__( 69 | {"type": "replace", "data": {"content": error_message}} 70 | ) 71 | await __event_call__(error_message) 72 | return error_message 73 | 74 | self.applet_file_id = matching_file.id 75 | 76 | # Simulate a loading process 77 | loading_messages = [ 78 | "Applet file found... launching", 79 | ] 80 | for message in loading_messages: 81 | await __event_emitter__( 82 | {"type": "replace", "data": {"content": message}} 83 | ) 84 | await asyncio.sleep(1) 85 | 86 | # Finally, replace with the actual applet embed 87 | final_message = f"{{{{HTML_FILE_ID_{self.applet_file_id}}}}}" 88 | await __event_emitter__( 89 | {"type": "replace", "data": {"content": final_message}} 90 | ) 91 | 92 | # Simulate a short delay to ensure the message is displayed 93 | await sleep(0.5) 94 | 95 | return "" 96 | 97 | except Exception as e: 98 | error_message = f"An error occurred while launching the applet: {str(e)}" 99 | await __event_emitter__( 100 | {"type": "replace", "data": {"content": error_message}} 101 | ) 102 | await __event_call__(error_message) 103 | return error_message 104 | -------------------------------------------------------------------------------- /plugins/pong/style.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin: 0; 3 | padding: 0; 4 | overflow: hidden; 5 | color: #ecf0f1; 6 | font-family: 'Arial', sans-serif; 7 | width: 100%; 8 | height: 100%; 9 | background-color: transparent; 10 | } 11 | #game-container { 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | width: 100%; 16 | padding-top: 10px; 17 | } 18 | #pong-board { 19 | border: 4px solid #ecf0f1; 20 | cursor: none; 21 | } 22 | #info { 23 | background-color: rgba(0, 0, 0, 0.5); 24 | padding: 5px 10px; 25 | border-radius: 5px; 26 | margin-bottom: 5px; 27 | } 28 | #new-game-btn { 29 | margin-top: 10px; 30 | padding: 5px 10px; 31 | background-color: #2ecc71; 32 | color: #fff; 33 | border: none; 34 | border-radius: 5px; 35 | cursor: pointer; 36 | } 37 | #new-game-btn:hover { 38 | background-color: #27ae60; 39 | } -------------------------------------------------------------------------------- /plugins/snake/app.js: -------------------------------------------------------------------------------- 1 | const canvas = document.getElementById('snake-board'); 2 | const ctx = canvas.getContext('2d'); 3 | const scoreElement = document.getElementById('score-value'); 4 | const levelElement = document.getElementById('level-value'); 5 | const newGameBtn = document.getElementById('new-game-btn'); 6 | 7 | const ROWS = 20; 8 | const COLS = 20; 9 | const BLOCK_SIZE = 20; 10 | 11 | canvas.width = COLS * BLOCK_SIZE; 12 | canvas.height = ROWS * BLOCK_SIZE; 13 | 14 | let snake = [{x: 10, y: 10}]; 15 | let food = null; 16 | let direction = {x: 1, y: 0}; 17 | let score = 0; 18 | let level = 1; 19 | let gameInterval = null; 20 | let gameSpeed = 200; 21 | let gameRunning = false; 22 | 23 | function drawBlock(x, y, color) { 24 | ctx.fillStyle = color; 25 | ctx.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); 26 | ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; 27 | ctx.strokeRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); 28 | } 29 | 30 | function drawSnake() { 31 | snake.forEach((segment, index) => { 32 | const color = index === 0 ? '#00ff00' : '#008000'; 33 | drawBlock(segment.x, segment.y, color); 34 | }); 35 | } 36 | 37 | function drawFood() { 38 | if (food) { 39 | drawBlock(food.x, food.y, '#ff0000'); 40 | } 41 | } 42 | 43 | function drawGrid() { 44 | ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; 45 | ctx.lineWidth = 0.5; 46 | for (let y = 0; y <= ROWS; y++) { 47 | ctx.beginPath(); 48 | ctx.moveTo(0, y * BLOCK_SIZE); 49 | ctx.lineTo(COLS * BLOCK_SIZE, y * BLOCK_SIZE); 50 | ctx.stroke(); 51 | } 52 | for (let x = 0; x <= COLS; x++) { 53 | ctx.beginPath(); 54 | ctx.moveTo(x * BLOCK_SIZE, 0); 55 | ctx.lineTo(x * BLOCK_SIZE, ROWS * BLOCK_SIZE); 56 | ctx.stroke(); 57 | } 58 | } 59 | 60 | function spawnFood() { 61 | food = { 62 | x: Math.floor(Math.random() * COLS), 63 | y: Math.floor(Math.random() * ROWS) 64 | }; 65 | while (snake.some(segment => segment.x === food.x && segment.y === food.y)) { 66 | food = { 67 | x: Math.floor(Math.random() * COLS), 68 | y: Math.floor(Math.random() * ROWS) 69 | }; 70 | } 71 | } 72 | 73 | function moveSnake() { 74 | const head = {x: snake[0].x + direction.x, y: snake[0].y + direction.y}; 75 | 76 | // Wrap around the board 77 | head.x = (head.x + COLS) % COLS; 78 | head.y = (head.y + ROWS) % ROWS; 79 | 80 | snake.unshift(head); 81 | 82 | if (head.x === food.x && head.y === food.y) { 83 | score += 10; 84 | scoreElement.textContent = score; 85 | if (score >= level * 100) { 86 | level++; 87 | levelElement.textContent = level; 88 | gameSpeed = Math.max(50, 200 - (level - 1) * 10); 89 | clearInterval(gameInterval); 90 | gameInterval = setInterval(gameLoop, gameSpeed); 91 | } 92 | spawnFood(); 93 | } else { 94 | snake.pop(); 95 | } 96 | } 97 | 98 | function checkCollision() { 99 | const head = snake[0]; 100 | for (let i = 1; i < snake.length; i++) { 101 | if (snake[i].x === head.x && snake[i].y === head.y) { 102 | return true; 103 | } 104 | } 105 | return false; 106 | } 107 | 108 | function gameLoop() { 109 | ctx.clearRect(0, 0, canvas.width, canvas.height); 110 | drawGrid(); 111 | moveSnake(); 112 | if (checkCollision()) { 113 | endGame(); 114 | return; 115 | } 116 | drawSnake(); 117 | drawFood(); 118 | } 119 | 120 | function resetGame() { 121 | snake = [{x: 10, y: 10}]; 122 | direction = {x: 1, y: 0}; 123 | score = 0; 124 | level = 1; 125 | gameSpeed = 200; 126 | scoreElement.textContent = score; 127 | levelElement.textContent = level; 128 | spawnFood(); 129 | } 130 | 131 | function startGame() { 132 | if (gameRunning) return; 133 | resetGame(); 134 | gameRunning = true; 135 | gameInterval = setInterval(gameLoop, gameSpeed); 136 | newGameBtn.textContent = "Restart Game"; 137 | } 138 | 139 | function endGame() { 140 | clearInterval(gameInterval); 141 | gameRunning = false; 142 | alert('Game Over! Your score: ' + score); 143 | newGameBtn.textContent = "New Game"; 144 | } 145 | 146 | function handleKeyPress(event) { 147 | if (!gameRunning) return; 148 | 149 | // Prevent default behavior only for arrow keys 150 | if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) { 151 | event.preventDefault(); 152 | } 153 | 154 | switch (event.key) { 155 | case 'ArrowLeft': 156 | if (direction.x === 0) direction = {x: -1, y: 0}; 157 | break; 158 | case 'ArrowRight': 159 | if (direction.x === 0) direction = {x: 1, y: 0}; 160 | break; 161 | case 'ArrowUp': 162 | if (direction.y === 0) direction = {x: 0, y: -1}; 163 | break; 164 | case 'ArrowDown': 165 | if (direction.y === 0) direction = {x: 0, y: 1}; 166 | break; 167 | } 168 | } 169 | 170 | // Use capturing phase to intercept events before they reach the parent document 171 | document.addEventListener('keydown', handleKeyPress, true); 172 | newGameBtn.addEventListener('click', startGame); 173 | 174 | // Initial draw 175 | drawGrid(); -------------------------------------------------------------------------------- /plugins/snake/snake_capp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Snake 9 | 10 | 11 | 12 |
13 |
14 |

Controls

15 |
    16 |
  • ← Move Left
  • 17 |
  • → Move Right
  • 18 |
  • ↑ Move Up
  • 19 |
  • ↓ Move Down
  • 20 |
21 |
22 |
23 | 24 |
25 |
Score: 0
26 |
Level: 1
27 |
28 | 29 |
30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /plugins/snake/snake_capp.py: -------------------------------------------------------------------------------- 1 | """ 2 | title: Snake 3 | author: Andrew Tait Gehrhardt 4 | author_url: https://github.com/atgehrhardt/Cerebro-OpenWebUI-Package-Manager/plugins/snake 5 | funding_url: https://github.com/open-webui 6 | version: 0.1.0 7 | """ 8 | 9 | import asyncio 10 | from asyncio import sleep 11 | from pydantic import BaseModel, Field 12 | from typing import Optional, Union, Generator, Iterator 13 | from apps.webui.models.files import Files 14 | from config import UPLOAD_DIR 15 | 16 | 17 | class Tools: 18 | """ 19 | Launches a game of Snake 20 | """ 21 | 22 | class Valves(BaseModel): 23 | priority: int = Field( 24 | default=0, description="Priority level for the filter operations." 25 | ) 26 | 27 | def __init__(self): 28 | self.valves = self.Valves() 29 | self.package_name = "snake" 30 | self.applet_file_id = None 31 | 32 | async def run( 33 | self, 34 | body: Optional[dict] = None, 35 | __user__: Optional[dict] = None, 36 | __event_emitter__: Optional[callable] = None, 37 | __event_call__: Optional[callable] = None, 38 | ) -> str: 39 | """ 40 | Retrieves information about the weather 41 | :param body: The request body. 42 | :param __user__: User information, including the user ID. 43 | :param __event_emitter__: Function to emit events during the process. 44 | :param __event_call__: Function to call for the final output. 45 | :return: The final message or an empty string. 46 | """ 47 | if not __user__ or "id" not in __user__: 48 | return "Error: User ID not provided" 49 | if not __event_emitter__ or not __event_call__: 50 | return "Error: Event emitter or event call not provided" 51 | 52 | user_id = __user__["id"] 53 | 54 | try: 55 | expected_filename = f"{UPLOAD_DIR}/cerebro/plugins/{self.package_name}/{self.package_name}_capp.html" 56 | all_files = Files.get_files() 57 | matching_file = next( 58 | ( 59 | file 60 | for file in all_files 61 | if file.user_id == user_id and file.filename == expected_filename 62 | ), 63 | None, 64 | ) 65 | 66 | if not matching_file: 67 | error_message = f"Error: Applet file for {self.package_name} not found. Make sure the package is installed." 68 | await __event_emitter__( 69 | {"type": "replace", "data": {"content": error_message}} 70 | ) 71 | await __event_call__(error_message) 72 | return error_message 73 | 74 | self.applet_file_id = matching_file.id 75 | 76 | # Simulate a loading process 77 | loading_messages = [ 78 | "Applet file found... launching", 79 | ] 80 | for message in loading_messages: 81 | await __event_emitter__( 82 | {"type": "replace", "data": {"content": message}} 83 | ) 84 | await asyncio.sleep(1) 85 | 86 | # Finally, replace with the actual applet embed 87 | final_message = f"{{{{HTML_FILE_ID_{self.applet_file_id}}}}}" 88 | await __event_emitter__( 89 | {"type": "replace", "data": {"content": final_message}} 90 | ) 91 | 92 | # Simulate a short delay to ensure the message is displayed 93 | await sleep(0.5) 94 | 95 | return "" 96 | 97 | except Exception as e: 98 | error_message = f"An error occurred while launching the applet: {str(e)}" 99 | await __event_emitter__( 100 | {"type": "replace", "data": {"content": error_message}} 101 | ) 102 | await __event_call__(error_message) 103 | return error_message 104 | -------------------------------------------------------------------------------- /plugins/snake/style.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin: 0; 3 | padding: 0; 4 | overflow: hidden; 5 | color: #ecf0f1; 6 | font-family: 'Arial', sans-serif; 7 | width: 100%; 8 | height: 100%; 9 | background-color: transparent; 10 | } 11 | #game-container { 12 | display: flex; 13 | justify-content: center; 14 | align-items: flex-start; 15 | padding: 10px; 16 | width: 100%; 17 | height: 100%; 18 | position: relative; 19 | z-index: 1; 20 | } 21 | #controls { 22 | position: absolute; 23 | left: 10px; 24 | top: 10px; 25 | width: 120px; 26 | background-color: rgba(0, 0, 0, 0.5); 27 | padding: 10px; 28 | border-radius: 5px; 29 | } 30 | #controls h3 { 31 | margin-top: 0; 32 | } 33 | #controls ul { 34 | padding-left: 20px; 35 | font-size: 12px; 36 | } 37 | #game-area { 38 | display: flex; 39 | flex-direction: column; 40 | align-items: center; 41 | } 42 | #snake-board { 43 | border: 4px solid #ecf0f1; 44 | } 45 | #info { 46 | display: flex; 47 | justify-content: space-between; 48 | width: 100%; 49 | max-width: 200px; 50 | margin-top: 10px; 51 | background-color: rgba(0, 0, 0, 0.5); 52 | padding: 5px; 53 | border-radius: 5px; 54 | } 55 | #new-game-btn { 56 | margin-top: 10px; 57 | padding: 5px 10px; 58 | background-color: #2ecc71; 59 | color: #fff; 60 | border: none; 61 | border-radius: 5px; 62 | cursor: pointer; 63 | } 64 | #new-game-btn:hover { 65 | background-color: #27ae60; 66 | } -------------------------------------------------------------------------------- /plugins/template/app.js: -------------------------------------------------------------------------------- 1 | function consumeEvent(event) { 2 | event.preventDefault(); 3 | event.stopPropagation(); 4 | console.log('Key captured: ' + event.key); 5 | } 6 | 7 | window.addEventListener('keydown', consumeEvent, true); 8 | window.addEventListener('keyup', consumeEvent, true); -------------------------------------------------------------------------------- /plugins/template/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: transparent; 3 | margin: 0; 4 | padding: 0; 5 | display: flex; 6 | justify-content: flex-end; 7 | align-items: center; 8 | min-height: 100vh; 9 | } 10 | 11 | .card { 12 | background-color: #2c3e50; 13 | border-radius: 15px; 14 | padding: 20px; 15 | width: 300px; 16 | margin-right: 20px; /* Moved more to the left */ 17 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 18 | color: white; 19 | font-family: Arial, sans-serif; 20 | } 21 | 22 | .card h2 { 23 | margin-top: 0; 24 | border-bottom: 1px solid #34495e; 25 | padding-bottom: 10px; 26 | } 27 | 28 | .card p { 29 | font-size: 14px; 30 | line-height: 1.6; 31 | } -------------------------------------------------------------------------------- /plugins/template/template_capp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Template 9 | 10 | 11 | 12 |
13 |

Template Title

14 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam in dui mauris. Vivamus hendrerit arcu sed erat molestie vehicula.

15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /plugins/template/template_capp.py: -------------------------------------------------------------------------------- 1 | """ 2 | title: Template 3 | author: Andrew Tait Gehrhardt 4 | author_url: https://github.com/atgehrhardt/Cerebro-OpenWebUI-Package-Manager/plugins/template 5 | funding_url: https://github.com/open-webui 6 | version: 0.1.0 7 | """ 8 | 9 | import asyncio 10 | from asyncio import sleep 11 | from pydantic import BaseModel, Field 12 | from typing import Optional, Union, Generator, Iterator 13 | from apps.webui.models.files import Files 14 | from config import UPLOAD_DIR 15 | 16 | 17 | class Tools: 18 | """ 19 | Launches the applet example template file 20 | """ 21 | 22 | class Valves(BaseModel): 23 | priority: int = Field( 24 | default=0, description="Priority level for the filter operations." 25 | ) 26 | 27 | def __init__(self): 28 | self.valves = self.Valves() 29 | self.package_name = "template" 30 | self.applet_file_id = None 31 | 32 | async def run( 33 | self, 34 | body: Optional[dict] = None, 35 | __user__: Optional[dict] = None, 36 | __event_emitter__: Optional[callable] = None, 37 | __event_call__: Optional[callable] = None, 38 | ) -> str: 39 | """ 40 | Retrieves information about the weather 41 | :param body: The request body. 42 | :param __user__: User information, including the user ID. 43 | :param __event_emitter__: Function to emit events during the process. 44 | :param __event_call__: Function to call for the final output. 45 | :return: The final message or an empty string. 46 | """ 47 | if not __user__ or "id" not in __user__: 48 | return "Error: User ID not provided" 49 | if not __event_emitter__ or not __event_call__: 50 | return "Error: Event emitter or event call not provided" 51 | 52 | user_id = __user__["id"] 53 | 54 | try: 55 | expected_filename = f"{UPLOAD_DIR}/cerebro/plugins/{self.package_name}/{self.package_name}_capp.html" 56 | all_files = Files.get_files() 57 | matching_file = next( 58 | ( 59 | file 60 | for file in all_files 61 | if file.user_id == user_id and file.filename == expected_filename 62 | ), 63 | None, 64 | ) 65 | 66 | if not matching_file: 67 | error_message = f"Error: Applet file for {self.package_name} not found. Make sure the package is installed." 68 | await __event_emitter__( 69 | {"type": "replace", "data": {"content": error_message}} 70 | ) 71 | await __event_call__(error_message) 72 | return error_message 73 | 74 | self.applet_file_id = matching_file.id 75 | 76 | # Simulate a loading process 77 | loading_messages = [ 78 | "Applet file found... launching", 79 | ] 80 | for message in loading_messages: 81 | await __event_emitter__( 82 | {"type": "replace", "data": {"content": message}} 83 | ) 84 | await asyncio.sleep(1) 85 | 86 | # Finally, replace with the actual applet embed 87 | final_message = f"{{{{HTML_FILE_ID_{self.applet_file_id}}}}}" 88 | await __event_emitter__( 89 | {"type": "replace", "data": {"content": final_message}} 90 | ) 91 | 92 | # Simulate a short delay to ensure the message is displayed 93 | await sleep(0.5) 94 | 95 | return "" 96 | 97 | except Exception as e: 98 | error_message = f"An error occurred while launching the applet: {str(e)}" 99 | await __event_emitter__( 100 | {"type": "replace", "data": {"content": error_message}} 101 | ) 102 | await __event_call__(error_message) 103 | return error_message 104 | -------------------------------------------------------------------------------- /plugins/tetris/app.js: -------------------------------------------------------------------------------- 1 | const canvas = document.getElementById('tetris-board'); 2 | const ctx = canvas.getContext('2d'); 3 | const scoreElement = document.getElementById('score-value'); 4 | const levelElement = document.getElementById('level-value'); 5 | 6 | const ROWS = 20; 7 | const COLS = 10; 8 | const BLOCK_SIZE = 15; 9 | 10 | canvas.width = COLS * BLOCK_SIZE; 11 | canvas.height = ROWS * BLOCK_SIZE; 12 | 13 | const COLORS = [ 14 | '#ff00ff', '#00ffff', '#ffff00', '#ff0000', '#00ff00', '#0000ff', '#ff8000' 15 | ]; 16 | 17 | let board = Array(ROWS).fill().map(() => Array(COLS).fill(0)); 18 | let currentPiece = null; 19 | let score = 0; 20 | let level = 1; 21 | let dropCounter = 0; 22 | let dropInterval = 1000; 23 | 24 | const SHAPES = [ 25 | [[1, 1, 1, 1]], 26 | [[1, 1], [1, 1]], 27 | [[1, 1, 1], [0, 1, 0]], 28 | [[1, 1, 1], [1, 0, 0]], 29 | [[1, 1, 1], [0, 0, 1]], 30 | [[1, 1, 0], [0, 1, 1]], 31 | [[0, 1, 1], [1, 1, 0]] 32 | ]; 33 | 34 | function createPiece() { 35 | const shape = SHAPES[Math.floor(Math.random() * SHAPES.length)]; 36 | const color = COLORS[Math.floor(Math.random() * COLORS.length)]; 37 | return { 38 | shape, 39 | color, 40 | x: Math.floor(COLS / 2) - Math.floor(shape[0].length / 2), 41 | y: 0 42 | }; 43 | } 44 | 45 | function drawBlock(x, y, color) { 46 | ctx.fillStyle = color; 47 | ctx.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); 48 | ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; 49 | ctx.strokeRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); 50 | 51 | // Add glow effect 52 | ctx.shadowColor = color; 53 | ctx.shadowBlur = 5; 54 | ctx.fillRect(x * BLOCK_SIZE + 1, y * BLOCK_SIZE + 1, BLOCK_SIZE - 2, BLOCK_SIZE - 2); 55 | ctx.shadowBlur = 0; 56 | } 57 | 58 | function drawBoard() { 59 | for (let y = 0; y < ROWS; y++) { 60 | for (let x = 0; x < COLS; x++) { 61 | if (board[y][x]) { 62 | drawBlock(x, y, board[y][x]); 63 | } 64 | } 65 | } 66 | } 67 | 68 | function drawGridlines() { 69 | ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; 70 | ctx.lineWidth = 0.5; 71 | for (let y = 0; y <= ROWS; y++) { 72 | ctx.beginPath(); 73 | ctx.moveTo(0, y * BLOCK_SIZE); 74 | ctx.lineTo(COLS * BLOCK_SIZE, y * BLOCK_SIZE); 75 | ctx.stroke(); 76 | } 77 | for (let x = 0; x <= COLS; x++) { 78 | ctx.beginPath(); 79 | ctx.moveTo(x * BLOCK_SIZE, 0); 80 | ctx.lineTo(x * BLOCK_SIZE, ROWS * BLOCK_SIZE); 81 | ctx.stroke(); 82 | } 83 | } 84 | 85 | function drawPiece() { 86 | currentPiece.shape.forEach((row, y) => { 87 | row.forEach((value, x) => { 88 | if (value) { 89 | drawBlock(currentPiece.x + x, currentPiece.y + y, currentPiece.color); 90 | } 91 | }); 92 | }); 93 | } 94 | 95 | function collision() { 96 | return currentPiece.shape.some((row, y) => { 97 | return row.some((value, x) => { 98 | const boardX = currentPiece.x + x; 99 | const boardY = currentPiece.y + y; 100 | return value && (boardY >= ROWS || boardX < 0 || boardX >= COLS || board[boardY][boardX]); 101 | }); 102 | }); 103 | } 104 | 105 | function mergePiece() { 106 | currentPiece.shape.forEach((row, y) => { 107 | row.forEach((value, x) => { 108 | if (value) { 109 | board[currentPiece.y + y][currentPiece.x + x] = currentPiece.color; 110 | } 111 | }); 112 | }); 113 | } 114 | 115 | function removeRows() { 116 | let rowsCleared = 0; 117 | for (let y = ROWS - 1; y >= 0; y--) { 118 | if (board[y].every(cell => cell !== 0)) { 119 | board.splice(y, 1); 120 | board.unshift(Array(COLS).fill(0)); 121 | rowsCleared++; 122 | y++; 123 | } 124 | } 125 | if (rowsCleared > 0) { 126 | score += rowsCleared * 100 * level; 127 | scoreElement.textContent = score; 128 | if (score >= level * 500) { 129 | level++; 130 | levelElement.textContent = level; 131 | dropInterval = Math.max(100, 1000 - (level - 1) * 100); 132 | } 133 | } 134 | } 135 | 136 | function rotate() { 137 | const rotated = currentPiece.shape[0].map((_, i) => 138 | currentPiece.shape.map(row => row[i]).reverse() 139 | ); 140 | const previousShape = currentPiece.shape; 141 | currentPiece.shape = rotated; 142 | if (collision()) { 143 | currentPiece.shape = previousShape; 144 | } 145 | } 146 | 147 | function moveDown() { 148 | currentPiece.y++; 149 | if (collision()) { 150 | currentPiece.y--; 151 | mergePiece(); 152 | removeRows(); 153 | currentPiece = createPiece(); 154 | if (collision()) { 155 | alert('Game Over! Your score: ' + score); 156 | board = Array(ROWS).fill().map(() => Array(COLS).fill(0)); 157 | score = 0; 158 | level = 1; 159 | dropInterval = 1000; 160 | scoreElement.textContent = score; 161 | levelElement.textContent = level; 162 | } 163 | } 164 | dropCounter = 0; 165 | } 166 | 167 | function moveToBottom() { 168 | while (!collision()) { 169 | currentPiece.y++; 170 | } 171 | currentPiece.y--; 172 | mergePiece(); 173 | removeRows(); 174 | currentPiece = createPiece(); 175 | if (collision()) { 176 | alert('Game Over! Your score: ' + score); 177 | board = Array(ROWS).fill().map(() => Array(COLS).fill(0)); 178 | score = 0; 179 | level = 1; 180 | dropInterval = 1000; 181 | scoreElement.textContent = score; 182 | levelElement.textContent = level; 183 | } 184 | } 185 | 186 | function moveLeft() { 187 | currentPiece.x--; 188 | if (collision()) { 189 | currentPiece.x++; 190 | } 191 | } 192 | 193 | function moveRight() { 194 | currentPiece.x++; 195 | if (collision()) { 196 | currentPiece.x--; 197 | } 198 | } 199 | 200 | function draw() { 201 | ctx.clearRect(0, 0, canvas.width, canvas.height); 202 | drawGridlines(); 203 | drawBoard(); 204 | drawPiece(); 205 | } 206 | 207 | let lastTime = 0; 208 | function gameLoop(time = 0) { 209 | const deltaTime = time - lastTime; 210 | lastTime = time; 211 | 212 | dropCounter += deltaTime; 213 | if (dropCounter > dropInterval) { 214 | moveDown(); 215 | } 216 | 217 | draw(); 218 | requestAnimationFrame(gameLoop); 219 | } 220 | 221 | function handleKeyPress(event) { 222 | event.preventDefault(); 223 | event.stopPropagation(); 224 | console.log('Key captured: ' + event.key); 225 | 226 | switch (event.key) { 227 | case 'ArrowLeft': 228 | moveLeft(); 229 | break; 230 | case 'ArrowRight': 231 | moveRight(); 232 | break; 233 | case 'ArrowDown': 234 | moveDown(); 235 | break; 236 | case 'ArrowUp': 237 | moveToBottom(); 238 | break; 239 | case ' ': 240 | rotate(); 241 | break; 242 | } 243 | } 244 | 245 | window.addEventListener('keydown', handleKeyPress, true); 246 | window.addEventListener('keyup', (event) => { 247 | event.preventDefault(); 248 | event.stopPropagation(); 249 | }, true); 250 | 251 | currentPiece = createPiece(); 252 | gameLoop(); -------------------------------------------------------------------------------- /plugins/tetris/style.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin: 0; 3 | padding: 0; 4 | overflow: hidden; 5 | color: #ecf0f1; 6 | font-family: 'Arial', sans-serif; 7 | width: 100%; 8 | height: 100%; 9 | background-color: transparent; 10 | } 11 | #game-container { 12 | display: flex; 13 | justify-content: center; 14 | align-items: flex-start; 15 | padding: 10px; 16 | width: 100%; 17 | height: 100%; 18 | position: relative; 19 | z-index: 1; 20 | } 21 | #controls { 22 | position: absolute; 23 | left: 10px; 24 | top: 10px; 25 | width: 120px; 26 | background-color: rgba(0, 0, 0, 0.5); 27 | padding: 10px; 28 | border-radius: 5px; 29 | } 30 | #controls h3 { 31 | margin-top: 0; 32 | } 33 | #controls ul { 34 | padding-left: 20px; 35 | font-size: 12px; 36 | } 37 | #game-area { 38 | display: flex; 39 | flex-direction: column; 40 | align-items: center; 41 | } 42 | #tetris-board { 43 | border: 2px solid rgba(255, 255, 255, 0.3); 44 | } 45 | #info { 46 | display: flex; 47 | justify-content: space-between; 48 | width: 100%; 49 | max-width: 200px; 50 | margin-top: 10px; 51 | background-color: rgba(0, 0, 0, 0.5); 52 | padding: 5px; 53 | border-radius: 5px; 54 | } -------------------------------------------------------------------------------- /plugins/tetris/tetris_capp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Tetris 9 | 10 | 11 | 12 |
13 |
14 |

Controls

15 |
    16 |
  • ← Move Left
  • 17 |
  • → Move Right
  • 18 |
  • ↓ Move Down
  • 19 |
  • ↑ Drop
  • 20 |
  • Space Rotate
  • 21 |
22 |
23 |
24 | 25 |
26 |
Score: 0
27 |
Level: 1
28 |
29 |
30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /plugins/tetris/tetris_capp.py: -------------------------------------------------------------------------------- 1 | """ 2 | title: Tetris 3 | author: Andrew Tait Gehrhardt 4 | author_url: https://github.com/atgehrhardt/Cerebro-OpenWebUI-Package-Manager/plugins/tetris 5 | funding_url: https://github.com/open-webui 6 | version: 0.1.0 7 | """ 8 | 9 | import asyncio 10 | from asyncio import sleep 11 | from pydantic import BaseModel, Field 12 | from typing import Optional 13 | from apps.webui.models.files import Files 14 | from config import UPLOAD_DIR 15 | 16 | 17 | class Tools: 18 | """ 19 | A tool that launches a game of tetris. You can use this to play the game tetris. 20 | """ 21 | 22 | class Valves(BaseModel): 23 | priority: int = Field( 24 | default=0, description="Priority level for the filter operations." 25 | ) 26 | 27 | def __init__(self): 28 | self.valves = self.Valves() 29 | self.package_name = "tetris" 30 | self.applet_file_id = None 31 | 32 | async def run( 33 | self, 34 | body: Optional[dict] = None, 35 | __user__: Optional[dict] = None, 36 | __event_emitter__: Optional[callable] = None, 37 | __event_call__: Optional[callable] = None, 38 | ) -> str: 39 | """ 40 | Launches a game of Tetris 41 | :param body: The request body. 42 | :param __user__: User information, including the user ID. 43 | :param __event_emitter__: Function to emit events during the process. 44 | :param __event_call__: Function to call for the final output. 45 | :return: The final message or an empty string. 46 | """ 47 | if not __user__ or "id" not in __user__: 48 | return "Error: User ID not provided" 49 | if not __event_emitter__ or not __event_call__: 50 | return "Error: Event emitter or event call not provided" 51 | 52 | user_id = __user__["id"] 53 | 54 | try: 55 | expected_filename = f"{UPLOAD_DIR}/cerebro/plugins/{self.package_name}/{self.package_name}_capp.html" 56 | all_files = Files.get_files() 57 | matching_file = next( 58 | ( 59 | file 60 | for file in all_files 61 | if file.user_id == user_id and file.filename == expected_filename 62 | ), 63 | None, 64 | ) 65 | 66 | if not matching_file: 67 | error_message = f"Error: Applet file for {self.package_name} not found. Make sure the package is installed." 68 | await __event_emitter__( 69 | {"type": "replace", "data": {"content": error_message}} 70 | ) 71 | await __event_call__(error_message) 72 | return error_message 73 | 74 | self.applet_file_id = matching_file.id 75 | 76 | # Simulate a loading process 77 | loading_messages = [ 78 | "Applet file found... launching", 79 | ] 80 | for message in loading_messages: 81 | await __event_emitter__( 82 | {"type": "replace", "data": {"content": message}} 83 | ) 84 | await asyncio.sleep(1) 85 | 86 | # Finally, replace with the actual applet embed 87 | final_message = f"{{{{HTML_FILE_ID_{self.applet_file_id}}}}}" 88 | await __event_emitter__( 89 | {"type": "replace", "data": {"content": final_message}} 90 | ) 91 | 92 | # Simulate a short delay to ensure the message is displayed 93 | await sleep(0.5) 94 | 95 | return f"Respond to the users that you have succesfully launched {self.package_name}" 96 | 97 | except Exception as e: 98 | error_message = f"An error occurred while launching the applet: {str(e)}" 99 | await __event_emitter__( 100 | {"type": "replace", "data": {"content": error_message}} 101 | ) 102 | await __event_call__(error_message) 103 | return error_message 104 | -------------------------------------------------------------------------------- /src/cerebro.py: -------------------------------------------------------------------------------- 1 | """ 2 | title: Cerebro Package Manager 3 | author: Andrew Tait Gehrhardt 4 | author_url: https://github.com/atgehrhardt/Cerebro-OpenWebUI-Package-Manager 5 | funding_url: https://github.com/open-webui 6 | version: 0.2.1 7 | 8 | ! ! ! 9 | IMPORTANT: THIS MUST BE THE SECOND TO LAST PRIORITY IN YOUR CHAIN. SET PRIORITY HIGHER THAN ALL 10 | OTHER FUNCTIONS EXCEPT FOR THE CEREBRO TOOL LAUNCHER 11 | ! ! ! 12 | 13 | Commands: 14 | `owui list` - List installed packages 15 | `owui install package_name` - Installs a package 16 | `owui uninstall package_name` - Uninstalls a package 17 | `owui update package_name` - Updates a package (uninstalls then reinstalls) 18 | `owui run package_name` - Runs an installed package in the chat window 19 | 20 | You can view all current package available for installation here: https://github.com/atgehrhardt/Cerebro-OpenWebUI-Package-Manager/tree/main/plugins 21 | """ 22 | 23 | from typing import List, Optional 24 | from pydantic import BaseModel, Field 25 | import requests 26 | import os 27 | import uuid 28 | import zipfile 29 | import io 30 | import shutil 31 | from urllib.parse import urlparse, urlunparse 32 | from apps.webui.models.files import Files 33 | from apps.webui.models.tools import Tools, ToolForm, ToolMeta 34 | 35 | from config import UPLOAD_DIR 36 | 37 | 38 | class Filter: 39 | SUPPORTED_COMMANDS = ["run", "install", "uninstall", "list", "update"] 40 | 41 | class Valves(BaseModel): 42 | priority: int = Field( 43 | default=1, description="Priority level for the filter operations." 44 | ) 45 | open_webui_host: str = os.getenv( 46 | "OPEN_WEBUI_HOST", 47 | "https://192.168.1.154", # If using Nginx this MUST be your server ip address https://192.168.1.xxx 48 | ) 49 | openai_base_url: str = os.getenv( 50 | "OPENAI_BASE_URL", "http://host.docker.internal:11434/v1" 51 | ) 52 | package_repo_url: str = os.getenv( 53 | "CEREBRO_PACKAGE_REPO_URL", 54 | "https://github.com/atgehrhardt/Cerebro-OpenWebUI-Package-Manager/tree/main/plugins", 55 | ) 56 | 57 | def __init__(self): 58 | self.valves = self.Valves() 59 | self.last_created_file = None 60 | self.selected_model = None 61 | self.user_id = None 62 | self.file = None 63 | self.package_files = {} 64 | self.pkg_launch = False 65 | self.installed_pkgs = [] 66 | self.packages = [] 67 | 68 | def check_tool_exists(self, tool_name: str) -> bool: 69 | tool_file = os.path.join( 70 | UPLOAD_DIR, "cerebro", "plugins", tool_name, f"{tool_name}_capp.py" 71 | ) 72 | return os.path.exists(tool_file) 73 | 74 | def uninstall_tool(self, tool_name: str): 75 | if not self.check_tool_exists(tool_name): 76 | print(f"Tool {tool_name} does not exist.") 77 | return 78 | 79 | try: 80 | # Get the tool by name 81 | tools = Tools.get_tools() 82 | tool = next((t for t in tools if t.name == tool_name), None) 83 | 84 | if tool: 85 | # Delete the tool from the database 86 | if Tools.delete_tool_by_id(tool.id): 87 | print( 88 | f"Tool {tool_name} uninstalled successfully from the database." 89 | ) 90 | else: 91 | print(f"Failed to uninstall tool {tool_name} from the database.") 92 | else: 93 | print(f"Tool {tool_name} not found in the database.") 94 | 95 | # Remove the tool file 96 | tool_file = os.path.join( 97 | UPLOAD_DIR, "cerebro", "plugins", tool_name, f"{tool_name}_capp.py" 98 | ) 99 | if os.path.exists(tool_file): 100 | os.remove(tool_file) 101 | print(f"Removed tool file: {tool_file}") 102 | 103 | self.pkg_launch = "Tool Uninstalled" 104 | except Exception as e: 105 | raise Exception(f"Error uninstalling tool {tool_name}: {str(e)}") 106 | 107 | def update_tool(self, tool_name: str): 108 | if not self.check_tool_exists(tool_name): 109 | print(f"Tool {tool_name} does not exist. Cannot update.") 110 | self.pkg_launch = "Tool Not Installed" 111 | return 112 | 113 | print(f"Updating tool {tool_name}...") 114 | try: 115 | # Uninstall the tool 116 | self.uninstall_tool(tool_name) 117 | 118 | # Install the tool again 119 | self.install_package(tool_name) # This will install both package and tool 120 | 121 | print(f"Tool {tool_name} updated successfully.") 122 | self.pkg_launch = "Tool Updated" 123 | except Exception as e: 124 | print(f"Error updating tool {tool_name}: {str(e)}") 125 | self.pkg_launch = "Tool Update Failed" 126 | raise Exception(f"Error updating tool {tool_name}: {str(e)}") 127 | 128 | def create_file( 129 | self, 130 | package_name, 131 | file_name: str, 132 | title: str, 133 | content: str, 134 | user_id: Optional[str] = None, 135 | ): 136 | user_id = user_id or self.user_id 137 | 138 | if not user_id: 139 | raise ValueError("User ID is required to create a file.") 140 | 141 | base_path = os.path.join(UPLOAD_DIR, "cerebro", "plugins", package_name) 142 | os.makedirs(base_path, exist_ok=True) 143 | 144 | file_id = str(uuid.uuid4()) 145 | file_path = os.path.join(base_path, file_name) 146 | 147 | try: 148 | with open(file_path, "w", encoding="utf-8") as f: 149 | print(f"Writing file to {file_path}...") 150 | f.write(content) 151 | except IOError as e: 152 | raise IOError(f"Error writing file to {file_path}: {str(e)}") 153 | 154 | try: 155 | meta = { 156 | "source": file_path, 157 | "title": title, 158 | "content_type": "text/html", 159 | "size": os.path.getsize(file_path), 160 | "path": file_path, 161 | } 162 | except FileNotFoundError as e: 163 | raise FileNotFoundError(f"File {file_path} not found: {str(e)}") 164 | 165 | class FileForm(BaseModel): 166 | id: str 167 | filename: str 168 | meta: dict = {} 169 | 170 | form_data = FileForm(id=file_id, filename=file_name, meta=meta) 171 | 172 | try: 173 | self.file = Files.insert_new_file(user_id, form_data) 174 | self.last_created_file = self.file 175 | return self.file 176 | except Exception as e: 177 | os.remove(file_path) 178 | raise Exception(f"Error inserting file into database: {str(e)}") 179 | 180 | def get_file_url(self, file_id: str) -> str: 181 | return f"{self.valves.open_webui_host}/api/v1/files/{file_id}/content" 182 | 183 | def handle_package(self, package_name, url: str, file_name: str): 184 | files = Files.get_files() 185 | files = [file for file in files if file.user_id == self.user_id] 186 | files = [file for file in files if file_name in file.filename] 187 | print("Files: ", files) 188 | 189 | if files: 190 | self.file = files[0].id 191 | print(f"\n{self.file}\n") 192 | print("File already exists") 193 | else: 194 | if not url: 195 | print("No URL provided, cannot download the file.") 196 | return 197 | 198 | try: 199 | print(f"Downloading the file from {url}...\n") 200 | response = requests.get(url) 201 | response.raise_for_status() 202 | file_content = response.text 203 | print("Downloaded file content:") 204 | print(file_content) 205 | except Exception as e: 206 | raise Exception(f"Error downloading {file_name}: {str(e)}") 207 | 208 | try: 209 | if not self.user_id: 210 | raise ValueError("User ID is not set. Cannot create file.") 211 | created_file = self.create_file( 212 | package_name, file_name, file_name, file_content, self.user_id 213 | ) 214 | self.file = ( 215 | created_file.id if hasattr(created_file, "id") else created_file 216 | ) 217 | except Exception as e: 218 | print(f"Error creating file: {str(e)}") 219 | raise Exception(f"Error creating file: {str(e)}") 220 | 221 | return self.file 222 | 223 | def is_package_installed(self, package_name: str) -> bool: 224 | package_dir = os.path.join(UPLOAD_DIR, "cerebro", "plugins", package_name) 225 | return os.path.exists(package_dir) 226 | 227 | def get_zip_url_from_tree_url(self, tree_url: str) -> str: 228 | parsed_url = urlparse(tree_url) 229 | path_parts = parsed_url.path.split("/") 230 | 231 | if "tree" in path_parts: 232 | tree_index = path_parts.index("tree") 233 | repo_path = "/".join(path_parts[:tree_index]) 234 | branch = path_parts[tree_index + 1] 235 | 236 | # Construct the raw zip URL 237 | zip_path = f"{repo_path}/archive/{branch}.zip" 238 | return urlunparse(parsed_url._replace(path=zip_path)) 239 | else: 240 | # If 'tree' is not in the URL, assume it's the main branch 241 | return f"{tree_url}/archive/main.zip" 242 | 243 | def get_subdirectory_from_tree_url(self, tree_url: str) -> str: 244 | parsed_url = urlparse(tree_url) 245 | path_parts = parsed_url.path.split("/") 246 | 247 | if "tree" in path_parts: 248 | tree_index = path_parts.index("tree") 249 | return "/".join(path_parts[tree_index + 2 :]) 250 | else: 251 | return "" 252 | 253 | def install_package(self, package_name: str): 254 | tree_url = self.valves.package_repo_url 255 | zip_url = self.get_zip_url_from_tree_url(tree_url) 256 | subdirectory = self.get_subdirectory_from_tree_url(tree_url) 257 | 258 | print(f"Tree URL: {tree_url}") 259 | print(f"Zip URL: {zip_url}") 260 | print(f"Subdirectory: {subdirectory}") 261 | 262 | if self.is_package_installed(package_name): 263 | print(f"Package {package_name} is already installed.") 264 | self.pkg_launch = "Already Installed" 265 | return 266 | 267 | try: 268 | # Download the zip file 269 | print(f"Downloading zip file from: {zip_url}") 270 | response = requests.get(zip_url) 271 | response.raise_for_status() 272 | 273 | # Get the repo name and branch from the url 274 | repo_name = tree_url.split("/")[-4] 275 | branch = tree_url.split("/")[-2] 276 | 277 | # Extract the specific package directory 278 | with zipfile.ZipFile(io.BytesIO(response.content)) as zip_ref: 279 | all_files = zip_ref.namelist() 280 | print(f"Files in zip: {all_files}") 281 | 282 | # Try to find the package directory 283 | package_dir = None 284 | for file in all_files: 285 | if file.endswith( 286 | f"{subdirectory}/{package_name}/" 287 | ) or file.endswith(f"{subdirectory}/{package_name}"): 288 | package_dir = file 289 | break 290 | 291 | if not package_dir: 292 | raise FileNotFoundError( 293 | f"Package directory for {package_name} not found in zip file" 294 | ) 295 | 296 | print(f"Found package directory: {package_dir}") 297 | 298 | # Extract the package files 299 | for file in all_files: 300 | if file.startswith(package_dir): 301 | print(f"Extracting {file}...") 302 | zip_ref.extract(file, UPLOAD_DIR) 303 | 304 | # Get the source directory 305 | src_dir = os.path.join(UPLOAD_DIR, package_dir) 306 | dst_dir = os.path.join(UPLOAD_DIR, "cerebro", "plugins", package_name) 307 | 308 | print(f"Source directory: {src_dir}") 309 | print(f"Destination directory: {dst_dir}") 310 | 311 | # Check if the source directory exists 312 | if not os.path.exists(src_dir): 313 | raise FileNotFoundError(f"Source directory not found: {src_dir}") 314 | 315 | # Create the destination directory if it doesn't exist 316 | os.makedirs(os.path.dirname(dst_dir), exist_ok=True) 317 | 318 | # Move the package directory to the plugins directory 319 | shutil.move(src_dir, dst_dir) 320 | 321 | # Remove the extracted directory 322 | extracted_dir = os.path.join(UPLOAD_DIR, f"{repo_name}-{branch}") 323 | shutil.rmtree(extracted_dir) 324 | 325 | # Loop through all the files in the package directory and create them in the database 326 | for root, dirs, files in os.walk(dst_dir): 327 | for file in files: 328 | file_path = os.path.join(root, file) 329 | print(f"Creating file: {file_path}") 330 | 331 | # Get the content of each file 332 | try: 333 | with open(file_path, "r", encoding="utf-8") as f: 334 | file_content = f.read() 335 | 336 | filename = os.path.basename(file_path) 337 | # Create file in the database 338 | created_file = self.create_file( 339 | package_name, 340 | f"{file_path}", 341 | f"{file_path}", 342 | file_content, 343 | self.user_id, 344 | ) 345 | 346 | self.package_files[filename] = ( 347 | created_file.id 348 | if hasattr(created_file, "id") 349 | else created_file 350 | ) 351 | except Exception as e: 352 | print(f"Error creating file: {str(e)}") 353 | raise Exception(f"Error creating file: {str(e)}") 354 | 355 | # Update the contents of _capp.html file 356 | capp_file = os.path.join(dst_dir, f"{package_name}_capp.html") 357 | if os.path.exists(capp_file): 358 | with open(capp_file, "r", encoding="utf-8") as f: 359 | capp_content = f.read() 360 | for filename, file_id in self.package_files.items(): 361 | # Replace the filename with the file content url 362 | capp_content = capp_content.replace( 363 | "{" + filename + "}", self.get_file_url(file_id) 364 | ) 365 | # Update the content of the _capp.html file 366 | with open(capp_file, "w", encoding="utf-8") as f: 367 | f.write(capp_content) 368 | else: 369 | print(f"Warning: {capp_file} not found. Skipping content update.") 370 | 371 | # Check for and install tool 372 | tool_file = os.path.join(dst_dir, f"{package_name}_capp.py") 373 | if os.path.exists(tool_file): 374 | print(f"Installing tool for package: {package_name}") 375 | with open(tool_file, "r", encoding="utf-8") as f: 376 | tool_content = f.read() 377 | 378 | # Prepend "cer_" to the tool name 379 | cer_tool_name = f"cer_{package_name}" 380 | 381 | # Extract the description from the tool content 382 | description = "Tool for " + package_name # Default description 383 | doc_string = self.extract_class_docstring(tool_content) 384 | if doc_string: 385 | description = doc_string.strip() 386 | 387 | # Create a ToolForm instance with the modified name and description 388 | tool_form = ToolForm( 389 | id=str(uuid.uuid4()), 390 | name=cer_tool_name, 391 | content=tool_content, 392 | meta=ToolMeta(description=description), 393 | ) 394 | 395 | # Insert the tool 396 | tool = Tools.insert_new_tool(self.user_id, tool_form, []) 397 | if tool: 398 | print( 399 | f"Tool for package {package_name} installed successfully as {cer_tool_name} with description: {description}" 400 | ) 401 | else: 402 | print(f"Failed to install tool for package {package_name}.") 403 | 404 | print(f"Package {package_name} installed successfully.") 405 | self.pkg_launch = "Installed" 406 | 407 | except Exception as e: 408 | print(f"Error installing package {package_name}: {str(e)}") 409 | raise Exception(f"Error installing package {package_name}: {str(e)}") 410 | 411 | def extract_class_docstring(self, content: str) -> Optional[str]: 412 | """ 413 | Extract the docstring of the first class in the given content. 414 | """ 415 | import ast 416 | 417 | try: 418 | tree = ast.parse(content) 419 | for node in ast.walk(tree): 420 | if isinstance(node, ast.ClassDef): 421 | docstring = ast.get_docstring(node) 422 | if docstring: 423 | return docstring 424 | except SyntaxError: 425 | print("Failed to parse the tool content") 426 | return None 427 | 428 | def update_package(self, package_name: str): 429 | if not self.is_package_installed(package_name): 430 | print(f"Package {package_name} is not installed. Cannot update.") 431 | self.pkg_launch = "Not Installed" 432 | return 433 | 434 | print(f"Updating package {package_name}...") 435 | try: 436 | # Uninstall the package (which will also uninstall the tool) 437 | self.uninstall_package(package_name) 438 | 439 | # Install the package again (which will also install the tool) 440 | self.install_package(package_name) 441 | 442 | print(f"Package {package_name} updated successfully.") 443 | self.pkg_launch = "Updated" 444 | except Exception as e: 445 | print(f"Error updating package {package_name}: {str(e)}") 446 | self.pkg_launch = "Update Failed" 447 | raise Exception(f"Error updating package {package_name}: {str(e)}") 448 | 449 | def uninstall_package(self, package_name: str): 450 | package_dir = os.path.join(UPLOAD_DIR, "cerebro", "plugins", package_name) 451 | if not os.path.exists(package_dir): 452 | print(f"Package {package_name} does not exist.") 453 | return 454 | 455 | try: 456 | # Get all files 457 | all_files = Files.get_files() 458 | 459 | # Filter files related to the package 460 | files_to_delete = [ 461 | file 462 | for file in all_files 463 | if file.user_id == self.user_id 464 | and f"/cerebro/plugins/{package_name}/" in file.filename 465 | ] 466 | 467 | # Delete files from the database 468 | deleted_count = 0 469 | for file in files_to_delete: 470 | if Files.delete_file_by_id(file.id): 471 | deleted_count += 1 472 | print(f"Deleted file: {file.filename}") 473 | 474 | print(f"Deleted {deleted_count} files from the database.") 475 | 476 | # Remove files and directories from the file system 477 | if os.path.exists(package_dir): 478 | shutil.rmtree(package_dir) 479 | print(f"Removed package directory: {package_dir}") 480 | else: 481 | print(f"Package directory {package_dir} does not exist.") 482 | 483 | # Uninstall tool 484 | tools = Tools.get_tools() 485 | tool_name = ( 486 | f"cer_{package_name}" # Look for the tool with the "cer_" prefix 487 | ) 488 | tool = next((t for t in tools if t.name == tool_name), None) 489 | 490 | if tool: 491 | if Tools.delete_tool_by_id(tool.id): 492 | print( 493 | f"Tool {tool_name} for package {package_name} uninstalled successfully." 494 | ) 495 | else: 496 | print( 497 | f"Failed to uninstall tool {tool_name} for package {package_name}." 498 | ) 499 | else: 500 | print( 501 | f"No tool found with name {tool_name} for package {package_name}." 502 | ) 503 | 504 | print(f"Package {package_name} uninstalled successfully.") 505 | self.pkg_launch = "Uninstalled" 506 | except Exception as e: 507 | raise Exception(f"Error uninstalling package {package_name}: {str(e)}") 508 | 509 | def list_packages(self, body: dict) -> List[str]: 510 | if not self.user_id: 511 | print("User ID is not set. Cannot list packages.") 512 | return [] 513 | 514 | plugins_dir = os.path.join(UPLOAD_DIR, "cerebro", "plugins") 515 | if not os.path.exists(plugins_dir): 516 | print("Plugins directory does not exist.") 517 | return [] 518 | 519 | self.packages = [ 520 | d 521 | for d in os.listdir(plugins_dir) 522 | if os.path.isdir(os.path.join(plugins_dir, d)) 523 | ] 524 | 525 | # Update installed packages based on both directory existence and database entries 526 | all_files = Files.get_files() 527 | db_packages = set( 528 | [ 529 | file.filename.split("/cerebro/plugins/")[1].split("/")[0] 530 | for file in all_files 531 | if file.user_id == self.user_id and "/cerebro/plugins/" in file.filename 532 | ] 533 | ) 534 | 535 | self.installed_pkgs = list(set(self.packages) | db_packages) 536 | 537 | print(f"\n\n\nPackages list: {self.installed_pkgs}\n\n\n") 538 | 539 | self.pkg_launch = "list" 540 | return self.installed_pkgs 541 | 542 | def list_packages(self, body: dict) -> List[str]: 543 | if not self.user_id: 544 | print("User ID is not set. Cannot list packages.") 545 | return [] 546 | 547 | plugins_dir = os.path.join(UPLOAD_DIR, "cerebro", "plugins") 548 | if not os.path.exists(plugins_dir): 549 | print("Plugins directory does not exist.") 550 | return [] 551 | 552 | self.packages = [ 553 | d 554 | for d in os.listdir(plugins_dir) 555 | if os.path.isdir(os.path.join(plugins_dir, d)) 556 | ] 557 | print(f"\n\n\nPackages list: {self.packages}\n\n\n") 558 | 559 | self.pkg_launch = "list" 560 | self.installed_pkgs = self.packages 561 | return self.packages 562 | 563 | def check_package_exists(self, package_name: str) -> bool: 564 | package_dir = os.path.join( 565 | UPLOAD_DIR, "cerebro", "plugins", package_name.replace("_capp.html", "") 566 | ) 567 | return os.path.exists(package_dir) 568 | 569 | def inlet(self, body: dict, __user__: Optional[dict] = None) -> dict: 570 | print(f"inlet:{__name__}") 571 | print(f"inlet:body:{body}") 572 | print(f"inlet:user:{__user__}") 573 | 574 | if __user__ and "id" in __user__: 575 | self.user_id = __user__["id"] 576 | else: 577 | print("Warning: No valid user ID provided") 578 | 579 | messages = body.get("messages", []) 580 | if messages: 581 | last_message = messages[-1]["content"] 582 | 583 | if last_message.startswith("owui "): 584 | command_parts = last_message.split() 585 | if len(command_parts) >= 2: 586 | command = command_parts[1] 587 | if command not in self.SUPPORTED_COMMANDS: 588 | self.pkg_launch = "invalid" 589 | 590 | if last_message.startswith("owui run"): 591 | command_parts = last_message.split() 592 | if len(command_parts) >= 3: 593 | _, _, package_name = command_parts[:3] 594 | url = ( 595 | " ".join(command_parts[3:]) 596 | if len(command_parts) > 3 597 | else None 598 | ) 599 | file_name = f"{package_name}_capp.html" 600 | print( 601 | f"Running command with file name: {file_name} and URL: {url}" 602 | ) 603 | 604 | if not self.check_package_exists(file_name): 605 | self.pkg_launch = "none" 606 | 607 | self.handle_package(package_name, url, file_name) 608 | self.pkg_launch = True 609 | 610 | elif last_message.startswith("owui install"): 611 | command_parts = last_message.split() 612 | if len(command_parts) >= 3: 613 | package_name = command_parts[2] 614 | print(f"Installing package: {package_name}") 615 | self.install_package(package_name) 616 | 617 | elif last_message.startswith("owui uninstall"): 618 | command_parts = last_message.split() 619 | if len(command_parts) >= 3: 620 | package_name = " ".join(command_parts[2:]) 621 | print(f"Uninstalling package: {package_name}") 622 | self.uninstall_package(package_name) 623 | 624 | elif last_message.startswith("owui list"): 625 | self.list_packages(body) 626 | 627 | elif last_message.startswith("owui update"): 628 | command_parts = last_message.split() 629 | if len(command_parts) >= 3: 630 | package_name = " ".join(command_parts[2:]) 631 | print(f"Updating package: {package_name}") 632 | self.update_package(package_name) 633 | 634 | return body 635 | 636 | def outlet(self, body: dict, __user__: Optional[dict] = None) -> dict: 637 | print(f"outlet:{__name__}") 638 | print(f"outlet:body:{body}") 639 | print(f"outlet:user:{__user__}") 640 | 641 | if self.pkg_launch is True: 642 | if self.file: 643 | body["messages"][-1]["content"] = f"{{{{HTML_FILE_ID_{self.file}}}}}" 644 | else: 645 | print("Error: File ID not set after handling package") 646 | body["messages"][-1]["content"] = "Error: Unable to load package" 647 | elif self.pkg_launch == "Installed": 648 | body["messages"][-1]["content"] = "Package Installed" 649 | elif self.pkg_launch == "Already Installed": 650 | body["messages"][-1]["content"] = "Package Already Installed" 651 | elif self.pkg_launch == "Uninstalled": 652 | body["messages"][-1]["content"] = "Package Uninstalled" 653 | elif self.pkg_launch == "Updated": 654 | body["messages"][-1]["content"] = "Package Updated Successfully" 655 | elif self.pkg_launch == "Update Failed": 656 | body["messages"][-1]["content"] = "Package Update Failed" 657 | elif self.pkg_launch == "Not Installed": 658 | body["messages"][-1]["content"] = "Package Not Installed. Cannot Update." 659 | elif self.pkg_launch == "list": 660 | body["messages"][-1]["content"] = ( 661 | "--- INSTALLED PACKAGES--- \n" + "\n".join(self.installed_pkgs) 662 | ) 663 | elif self.pkg_launch == "none": 664 | body["messages"][-1]["content"] = "Package Not installed" 665 | elif self.pkg_launch == "invalid": 666 | body["messages"][-1][ 667 | "content" 668 | ] = f"Invalid command. Supported commands are: {', '.join(self.SUPPORTED_COMMANDS)}" 669 | else: 670 | pass 671 | 672 | self.pkg_launch = False 673 | 674 | print("\n\n\n") 675 | return body 676 | -------------------------------------------------------------------------------- /src/cerebro_package_manager.json: -------------------------------------------------------------------------------- 1 | [{"id":"cerebro_package_manager","user_id":"5c81c4d9-7456-4fa6-b2cd-860cd3b13d61","name":"Cerebro Package Manager","type":"filter","content":"\"\"\"\ntitle: Cerebro Package Manager\nauthor: Andrew Tait Gehrhardt\nauthor_url: https://github.com/atgehrhardt/Cerebro-OpenWebUI-Package-Manager\nfunding_url: https://github.com/open-webui\nversion: 0.1.3\n\"\"\"\n\nfrom typing import List, Union, Generator, Iterator, Optional\nfrom pydantic import BaseModel\nimport requests\nimport os\nimport json\nimport aiohttp\nimport uuid\nimport re\nimport zipfile\nimport io\nimport shutil\nfrom utils.misc import get_last_user_message\nfrom apps.webui.models.files import Files\n\nfrom config import UPLOAD_DIR\n\n\nclass Filter:\n SUPPORTED_COMMANDS = [\"run\", \"install\", \"uninstall\", \"list\"]\n\n class Valves(BaseModel):\n open_webui_host: str = os.getenv(\n \"OPEN_WEBUI_HOST\",\n \"https://192.168.1.154\", # If using Nginx this MUST be your server ip address https://192.168.1.xxx\n )\n openai_base_url: str = os.getenv(\n \"OPENAI_BASE_URL\", \"http://host.docker.internal:11434/v1\"\n )\n package_repo_url: str = os.getenv(\n \"CEREBRO_PACKAGE_REPO_URL\",\n \"https://github.com/atgehrhardt/Cerebro-OpenWebUI-Package-Manager\",\n )\n\n def __init__(self):\n self.file_handler = True\n self.valves = self.Valves()\n self.last_created_file = None\n self.selected_model = None\n self.user_id = None\n self.file = None\n self.package_files = {}\n self.pkg_launch = False\n self.installed_pkgs = []\n self.packages = []\n\n def create_file(\n self,\n package_name,\n file_name: str,\n title: str,\n content: str,\n user_id: Optional[str] = None,\n ):\n user_id = user_id or self.user_id\n\n if not user_id:\n raise ValueError(\"User ID is required to create a file.\")\n\n base_path = os.path.join(UPLOAD_DIR, \"cerebro\", \"plugins\", package_name)\n os.makedirs(base_path, exist_ok=True)\n\n file_id = str(uuid.uuid4())\n file_path = os.path.join(base_path, file_name)\n\n try:\n with open(file_path, \"w\", encoding=\"utf-8\") as f:\n print(f\"Writing file to {file_path}...\")\n f.write(content)\n except IOError as e:\n raise IOError(f\"Error writing file to {file_path}: {str(e)}\")\n\n try:\n meta = {\n \"source\": file_path,\n \"title\": title,\n \"content_type\": \"text/html\",\n \"size\": os.path.getsize(file_path),\n \"path\": file_path,\n }\n except FileNotFoundError as e:\n raise FileNotFoundError(f\"File {file_path} not found: {str(e)}\")\n\n class FileForm(BaseModel):\n id: str\n filename: str\n meta: dict = {}\n\n form_data = FileForm(id=file_id, filename=file_name, meta=meta)\n\n try:\n self.file = Files.insert_new_file(user_id, form_data)\n self.last_created_file = self.file\n return self.file\n except Exception as e:\n os.remove(file_path)\n raise Exception(f\"Error inserting file into database: {str(e)}\")\n\n def get_file_url(self, file_id: str) -> str:\n return f\"{self.valves.open_webui_host}/api/v1/files/{file_id}/content\"\n\n def handle_package(self, package_name, url: str, file_name: str):\n files = Files.get_files()\n files = [file for file in files if file.user_id == self.user_id]\n files = [file for file in files if file_name in file.filename]\n print(\"Files: \", files)\n\n if files:\n self.file = files[0].id\n print(f\"\\n{self.file}\\n\")\n print(\"File already exists\")\n else:\n if not url:\n print(\"No URL provided, cannot download the file.\")\n return\n\n try:\n print(f\"Downloading the file from {url}...\\n\")\n response = requests.get(url)\n response.raise_for_status()\n file_content = response.text\n print(\"Downloaded file content:\")\n print(file_content)\n except Exception as e:\n raise Exception(f\"Error downloading {file_name}: {str(e)}\")\n\n try:\n if not self.user_id:\n raise ValueError(\"User ID is not set. Cannot create file.\")\n created_file = self.create_file(\n package_name, file_name, file_name, file_content, self.user_id\n )\n self.file = (\n created_file.id if hasattr(created_file, \"id\") else created_file\n )\n except Exception as e:\n print(f\"Error creating file: {str(e)}\")\n raise Exception(f\"Error creating file: {str(e)}\")\n\n return self.file\n\n def is_package_installed(self, package_name: str) -> bool:\n package_dir = os.path.join(UPLOAD_DIR, \"cerebro\", \"plugins\", package_name)\n return os.path.exists(package_dir)\n\n def install_package(self, package_name: str):\n zip_url = f\"{self.valves.package_repo_url}/archive/main.zip\"\n\n if self.is_package_installed(package_name):\n print(f\"Package {package_name} is already installed.\")\n self.pkg_launch = \"Already Installed\"\n return\n\n try:\n # Download the zip file\n response = requests.get(zip_url)\n response.raise_for_status()\n\n # Get the repo name from the url\n repo_name = self.valves.package_repo_url.split(\"/\")[-1]\n\n # Extract the specific package directory\n with zipfile.ZipFile(io.BytesIO(response.content)) as zip_ref:\n package_dir = f\"{repo_name}-main/plugins/{package_name}\"\n print(f\"Extracting package directory: {package_dir}...\")\n for file in zip_ref.namelist():\n if file.startswith(package_dir) and file != package_dir + \"/\":\n # Extract the package files\n print(f\"Extracting {file}...\")\n zip_ref.extract(file, UPLOAD_DIR)\n\n # Get the source directory\n src_dir = os.path.join(UPLOAD_DIR, package_dir)\n dst_dir = os.path.join(UPLOAD_DIR, \"cerebro\", \"plugins\", package_name)\n\n # Check if the source directory exists\n if not os.path.exists(src_dir):\n raise FileNotFoundError(f\"Source directory not found: {src_dir}\")\n\n # Create the destination directory if it doesn't exist\n os.makedirs(os.path.dirname(dst_dir), exist_ok=True)\n\n # Move the package directory to the plugins directory\n shutil.move(src_dir, dst_dir)\n\n # Remove the extracted directory\n shutil.rmtree(os.path.join(UPLOAD_DIR, f\"{repo_name}-main\"))\n\n # Loop through all the files in the package directory and create them in the database\n for root, dirs, files in os.walk(dst_dir):\n for file in files:\n file_path = os.path.join(root, file)\n print(f\"Creating file: {file_path}\")\n\n # Get the content of each file\n try:\n with open(file_path, \"r\", encoding=\"utf-8\") as f:\n file_content = f.read()\n\n filename = os.path.basename(file_path)\n # Create file in the database\n created_file = self.create_file(\n package_name,\n f\"{file_path}\",\n f\"{file_path}\",\n file_content,\n self.user_id,\n )\n\n self.package_files[filename] = (\n created_file.id\n if hasattr(created_file, \"id\")\n else created_file\n )\n except Exception as e:\n print(f\"Error creating file: {str(e)}\")\n raise Exception(f\"Error creating file: {str(e)}\")\n\n # Update the contents of _capp.html file\n capp_file = os.path.join(dst_dir, f\"{package_name}_capp.html\")\n with open(capp_file, \"r\", encoding=\"utf-8\") as f:\n capp_content = f.read()\n for filename, file_id in self.package_files.items():\n # Replace the filename with the file content url\n capp_content = capp_content.replace(\n \"{\" + filename + \"}\", self.get_file_url(file_id)\n )\n # Update the content of the _capp.html file\n with open(capp_file, \"w\", encoding=\"utf-8\") as f:\n f.write(capp_content)\n\n print(f\"Package {package_name} installed successfully.\")\n self.pkg_launch = \"Installed\"\n\n except Exception as e:\n print(f\"Error installing package {package_name}: {str(e)}\")\n raise Exception(f\"Error installing package {package_name}: {str(e)}\")\n\n def uninstall_package(self, package_name: str):\n package_dir = os.path.join(UPLOAD_DIR, \"cerebro\", \"plugins\", package_name)\n if not os.path.exists(package_dir):\n print(f\"Package {package_name} does not exist.\")\n return\n\n try:\n for root, dirs, files in os.walk(package_dir, topdown=False):\n for file in files:\n os.remove(os.path.join(root, file))\n for dir in dirs:\n os.rmdir(os.path.join(root, dir))\n os.rmdir(package_dir)\n\n # Remove the file from the database\n files = Files.get_files()\n files = [file for file in files if file.user_id == self.user_id]\n files = [\n file for file in files if f\"{package_name}_capp.html\" in file.filename\n ]\n if files:\n Files.delete_file_by_id(files[0].id)\n\n print(f\"Package {package_name} uninstalled successfully.\")\n self.pkg_launch = \"Uninstalled\"\n except Exception as e:\n raise Exception(f\"Error deleting package {package_name}: {str(e)}\")\n\n def list_packages(self, body: dict) -> List[str]:\n if not self.user_id:\n print(\"User ID is not set. Cannot list packages.\")\n return []\n\n plugins_dir = os.path.join(UPLOAD_DIR, \"cerebro\", \"plugins\")\n if not os.path.exists(plugins_dir):\n print(\"Plugins directory does not exist.\")\n return []\n\n self.packages = [\n d\n for d in os.listdir(plugins_dir)\n if os.path.isdir(os.path.join(plugins_dir, d))\n ]\n print(f\"\\n\\n\\nPackages list: {self.packages}\\n\\n\\n\")\n\n self.pkg_launch = \"list\"\n self.installed_pkgs = self.packages\n return self.packages\n\n def check_package_exists(self, package_name: str) -> bool:\n package_dir = os.path.join(\n UPLOAD_DIR, \"cerebro\", \"plugins\", package_name.replace(\"_capp.html\", \"\")\n )\n return os.path.exists(package_dir)\n\n def inlet(self, body: dict, __user__: Optional[dict] = None) -> dict:\n print(f\"inlet:{__name__}\")\n print(f\"inlet:body:{body}\")\n print(f\"inlet:user:{__user__}\")\n\n if __user__ and \"id\" in __user__:\n self.user_id = __user__[\"id\"]\n else:\n print(\"Warning: No valid user ID provided\")\n\n messages = body.get(\"messages\", [])\n if messages:\n last_message = messages[-1][\"content\"]\n print(f\"Last message: {last_message}\")\n\n if last_message.startswith(\"owui \"):\n command_parts = last_message.split()\n if len(command_parts) >= 2:\n command = command_parts[1]\n if command not in self.SUPPORTED_COMMANDS:\n self.pkg_launch = \"invalid\"\n return body\n\n if last_message.startswith(\"owui run\"):\n command_parts = last_message.split()\n print(f\"Command parts: {command_parts}\")\n if len(command_parts) >= 3:\n _, _, package_name = command_parts[:3]\n url = (\n \" \".join(command_parts[3:])\n if len(command_parts) > 3\n else None\n )\n file_name = f\"{package_name}_capp.html\"\n print(\n f\"Running command with file name: {file_name} and URL: {url}\"\n )\n\n if not self.check_package_exists(file_name):\n self.pkg_launch = \"none\"\n return body\n\n self.handle_package(package_name, url, file_name)\n self.pkg_launch = True\n return body\n\n elif last_message.startswith(\"owui install\"):\n command_parts = last_message.split()\n print(f\"Command parts: {command_parts}\")\n if len(command_parts) >= 3:\n package_name = command_parts[2]\n print(f\"Installing package: {package_name}\")\n self.install_package(package_name)\n return body\n\n elif last_message.startswith(\"owui uninstall\"):\n command_parts = last_message.split()\n print(f\"Command parts: {command_parts}\")\n if len(command_parts) >= 3:\n package_name = \" \".join(command_parts[2:])\n print(f\"Uninstalling package: {package_name}\")\n self.uninstall_package(package_name)\n return body\n\n elif last_message.startswith(\"owui list\"):\n self.list_packages(body)\n print(f\"\\n\\n\\nReturning body: {body}\\n\\n\\n\")\n return body\n return body\n\n def outlet(self, body: dict, __user__: Optional[dict] = None) -> dict:\n print(f\"outlet:{__name__}\")\n print(f\"outlet:body:{body}\")\n print(f\"outlet:user:{__user__}\")\n\n if self.pkg_launch is True:\n if self.file:\n body[\"messages\"][-1][\"content\"] = f\"{{{{HTML_FILE_ID_{self.file}}}}}\"\n else:\n print(\"Error: File ID not set after handling package\")\n body[\"messages\"][-1][\"content\"] = \"Error: Unable to load package\"\n elif self.pkg_launch == \"Installed\":\n body[\"messages\"][-1][\"content\"] = \"Package Installed\"\n elif self.pkg_launch == \"Already Installed\":\n body[\"messages\"][-1][\"content\"] = \"Package Already Installed\"\n elif self.pkg_launch == \"Uninstalled\":\n body[\"messages\"][-1][\"content\"] = \"Package Uninstalled\"\n elif self.pkg_launch == \"list\":\n body[\"messages\"][-1][\"content\"] = (\n \"--- INSTALLED PACKAGES--- \\n\" + \"\\n\".join(self.installed_pkgs)\n )\n elif self.pkg_launch == \"none\":\n body[\"messages\"][-1][\"content\"] = \"Package Not installed\"\n elif self.pkg_launch == \"invalid\":\n body[\"messages\"][-1][\n \"content\"\n ] = f\"Invalid command. Supported commands are: {', '.join(self.SUPPORTED_COMMANDS)}\"\n else:\n pass\n\n self.pkg_launch = False\n\n return body\n","meta":{"description":"Cerebro Package Manager","manifest":{"title":"Cerebro Package Manager","author":"Andrew Tait Gehrhardt","author_url":"https://github.com/atgehrhardt/Cerebro-OpenWebUI-Package-Manager","funding_url":"https://github.com/open-webui","version":"0.1.3"}},"is_active":true,"updated_at":1719468386,"created_at":1719468257}] -------------------------------------------------------------------------------- /src/cerebro_tool_launcher.json: -------------------------------------------------------------------------------- 1 | [{"id":"cerebro_tool_launcher","user_id":"5c81c4d9-7456-4fa6-b2cd-860cd3b13d61","name":"Cerebro Tool Launcher","type":"filter","content":"\"\"\"\ntitle: Cerebro Tool Launcher\nauthor: Andrew Tait Gehrhardt\nauthor_url: https://github.com/atgehrhardt/Cerebro-OpenWebUI-Package-Manager\nfunding_url: https://github.com/open-webui\nversion: 0.1.0\n\"\"\"\n\nfrom typing import List, Dict, Optional\nimport re\nimport uuid\nfrom apps.webui.models.files import Files\nimport requests\n\n\nclass Filter:\n def __init__(self):\n self.file = None\n self.user_id = None\n\n def inlet(self, body: dict, __user__: Optional[dict] = None) -> dict:\n print(f\"inlet:{__name__}\")\n print(f\"inlet:body:{body}\")\n print(f\"inlet:user:{__user__}\")\n if __user__ and \"id\" in __user__:\n self.user_id = __user__[\"id\"]\n else:\n print(\"Warning: No valid user ID provided\")\n return body\n\n def handle_package(self, package_name: str):\n files = Files.get_files()\n files = [file for file in files if file.user_id == self.user_id]\n files = [\n file\n for file in files\n if file.filename.endswith(f\"{package_name}_capp.html\")\n ]\n print(\"Files: \", files)\n if files:\n self.file = files[0].id\n print(f\"\\nFound file: {self.file}\\n\")\n return True\n else:\n print(f\"No matching file found for package: {package_name}\")\n return False\n\n def outlet(self, body: dict, __user__: Optional[dict] = None) -> dict:\n print(f\"outlet:{__name__}\")\n print(f\"outlet:body:{body}\")\n print(f\"outlet:user:{__user__}\")\n messages = body.get(\"messages\", [])\n if messages:\n last_message = messages[-1][\"content\"]\n owui_run_matches = re.finditer(r\"owui run (\\w+)\", last_message)\n\n new_content = last_message\n for match in owui_run_matches:\n package_name = match.group(1)\n print(f\"Detected 'owui run' command for package: {package_name}\")\n if self.handle_package(package_name):\n if self.file:\n replacement = f\"{{{{HTML_FILE_ID_{self.file}}}}}\"\n new_content = new_content.replace(match.group(0), replacement)\n self.file = None # Reset file ID after use\n else:\n print(\n f\"Error: File ID not set after handling package {package_name}\"\n )\n replacement = f\"Error: Unable to load package {package_name}\"\n new_content = new_content.replace(match.group(0), replacement)\n else:\n print(f\"Error: Failed to handle package {package_name}\")\n replacement = f\"Error: Failed to load package {package_name}\"\n new_content = new_content.replace(match.group(0), replacement)\n\n messages[-1][\"content\"] = new_content\n return body\n","meta":{"description":"Cerebro Tool Launcher","manifest":{"title":"Cerebro Tool Launcher","author":"Andrew Tait Gehrhardt","author_url":"https://github.com/atgehrhardt/Cerebro-OpenWebUI-Package-Manager","funding_url":"https://github.com/open-webui","version":"0.1.0"}},"is_active":true,"updated_at":1719468485,"created_at":1719468474}] -------------------------------------------------------------------------------- /src/cerebro_tool_launcher.py: -------------------------------------------------------------------------------- 1 | """ 2 | title: Cerebro Tool Launcher 3 | author: Andrew Tait Gehrhardt 4 | author_url: https://github.com/atgehrhardt/Cerebro-OpenWebUI-Package-Manager 5 | funding_url: https://github.com/open-webui 6 | version: 0.1.0 7 | """ 8 | 9 | """ 10 | EXAMPLE SYSTEM PROMPT: 11 | 12 | You have the ability to use tools to answer user queries. You can use the tools by responding with the command `owui run {tool_name}` 13 | 14 | If you use a tool ONLY RESPOND WITH THE COMMANDS AND NOTHING ELSE! 15 | You have access to the below tools: 16 | - package_name: LLM friendly description 17 | - package_name: LLM friendly description 18 | - package_name: LLM friendly description 19 | 20 | You can use multiple tools by responding: 21 | owui run {tool_name1} 22 | owui run {tool_name2} 23 | 24 | If the user is not inquiring about a topic that needs a tool, then you should NOT use one! 25 | 26 | You should never use a tool if all the user says is "Thanks" or "thanks". 27 | """ 28 | 29 | from typing import List, Dict, Optional 30 | import re 31 | import uuid 32 | from apps.webui.models.files import Files 33 | import requests 34 | 35 | 36 | class Filter: 37 | def __init__(self): 38 | self.file = None 39 | self.user_id = None 40 | 41 | def inlet(self, body: dict, __user__: Optional[dict] = None) -> dict: 42 | print(f"inlet:{__name__}") 43 | print(f"inlet:body:{body}") 44 | print(f"inlet:user:{__user__}") 45 | if __user__ and "id" in __user__: 46 | self.user_id = __user__["id"] 47 | else: 48 | print("Warning: No valid user ID provided") 49 | return body 50 | 51 | def handle_package(self, package_name: str): 52 | files = Files.get_files() 53 | files = [file for file in files if file.user_id == self.user_id] 54 | files = [ 55 | file 56 | for file in files 57 | if file.filename.endswith(f"{package_name}_capp.html") 58 | ] 59 | print("Files: ", files) 60 | if files: 61 | self.file = files[0].id 62 | print(f"\nFound file: {self.file}\n") 63 | return True 64 | else: 65 | print(f"No matching file found for package: {package_name}") 66 | return False 67 | 68 | def outlet(self, body: dict, __user__: Optional[dict] = None) -> dict: 69 | print(f"outlet:{__name__}") 70 | print(f"outlet:body:{body}") 71 | print(f"outlet:user:{__user__}") 72 | messages = body.get("messages", []) 73 | if messages: 74 | last_message = messages[-1]["content"] 75 | owui_run_matches = re.finditer(r"owui run (\w+)", last_message) 76 | 77 | new_content = last_message 78 | for match in owui_run_matches: 79 | package_name = match.group(1) 80 | print(f"Detected 'owui run' command for package: {package_name}") 81 | if self.handle_package(package_name): 82 | if self.file: 83 | replacement = f"{{{{HTML_FILE_ID_{self.file}}}}}" 84 | new_content = new_content.replace(match.group(0), replacement) 85 | self.file = None # Reset file ID after use 86 | else: 87 | print( 88 | f"Error: File ID not set after handling package {package_name}" 89 | ) 90 | replacement = f"Error: Unable to load package {package_name}" 91 | new_content = new_content.replace(match.group(0), replacement) 92 | else: 93 | print(f"Error: Failed to handle package {package_name}") 94 | replacement = f"Error: Failed to load package {package_name}" 95 | new_content = new_content.replace(match.group(0), replacement) 96 | 97 | messages[-1]["content"] = new_content 98 | return body 99 | --------------------------------------------------------------------------------