| 
									
										
										
										
											2025-02-10 18:25:02 +08:00
										 |  |  | import asyncio | 
					
						
							|  |  |  | import json | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  | import logging | 
					
						
							| 
									
										
										
										
											2025-02-10 18:25:02 +08:00
										 |  |  | import uuid | 
					
						
							| 
									
										
										
										
											2025-03-03 22:05:50 +08:00
										 |  |  | from typing import Optional | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  | import aiohttp | 
					
						
							| 
									
										
										
										
											2025-02-10 18:25:02 +08:00
										 |  |  | import websockets | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  | from pydantic import BaseModel | 
					
						
							| 
									
										
										
										
											2025-02-10 18:25:02 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-04 11:58:32 +08:00
										 |  |  | from open_webui.env import SRC_LOG_LEVELS | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  | logger = logging.getLogger(__name__) | 
					
						
							| 
									
										
										
										
											2025-03-04 11:58:32 +08:00
										 |  |  | logger.setLevel(SRC_LOG_LEVELS["MAIN"]) | 
					
						
							| 
									
										
										
										
											2025-02-10 18:25:02 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | class ResultModel(BaseModel): | 
					
						
							| 
									
										
										
										
											2025-02-10 18:25:02 +08:00
										 |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |     Execute Code Result Model | 
					
						
							| 
									
										
										
										
											2025-02-10 18:25:02 +08:00
										 |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |     stdout: Optional[str] = "" | 
					
						
							|  |  |  |     stderr: Optional[str] = "" | 
					
						
							|  |  |  |     result: Optional[str] = "" | 
					
						
							| 
									
										
										
										
											2025-03-03 22:05:50 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-10 18:25:02 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  | class JupyterCodeExecuter: | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Execute code in jupyter notebook | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2025-02-10 18:25:02 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-04 12:16:40 +08:00
										 |  |  |     def __init__( | 
					
						
							|  |  |  |         self, | 
					
						
							|  |  |  |         base_url: str, | 
					
						
							|  |  |  |         code: str, | 
					
						
							|  |  |  |         token: str = "", | 
					
						
							|  |  |  |         password: str = "", | 
					
						
							|  |  |  |         timeout: int = 60, | 
					
						
							|  |  |  |     ): | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         :param base_url: Jupyter server URL (e.g., "http://localhost:8888") | 
					
						
							|  |  |  |         :param code: Code to execute | 
					
						
							|  |  |  |         :param token: Jupyter authentication token (optional) | 
					
						
							|  |  |  |         :param password: Jupyter password (optional) | 
					
						
							|  |  |  |         :param timeout: WebSocket timeout in seconds (default: 60s) | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2025-05-10 02:32:01 +08:00
										 |  |  |         self.base_url = base_url | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |         self.code = code | 
					
						
							|  |  |  |         self.token = token | 
					
						
							|  |  |  |         self.password = password | 
					
						
							|  |  |  |         self.timeout = timeout | 
					
						
							|  |  |  |         self.kernel_id = "" | 
					
						
							| 
									
										
										
										
											2025-05-10 23:00:01 +08:00
										 |  |  |         if self.base_url[-1] != "/": | 
					
						
							|  |  |  |             self.base_url += "/" | 
					
						
							| 
									
										
										
										
											2025-04-28 20:47:34 +08:00
										 |  |  |         self.session = aiohttp.ClientSession(trust_env=True, base_url=self.base_url) | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |         self.params = {} | 
					
						
							|  |  |  |         self.result = ResultModel() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def __aenter__(self): | 
					
						
							|  |  |  |         return self | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def __aexit__(self, exc_type, exc_val, exc_tb): | 
					
						
							|  |  |  |         if self.kernel_id: | 
					
						
							|  |  |  |             try: | 
					
						
							| 
									
										
										
										
											2025-03-04 12:16:40 +08:00
										 |  |  |                 async with self.session.delete( | 
					
						
							| 
									
										
										
										
											2025-05-10 02:32:01 +08:00
										 |  |  |                     f"api/kernels/{self.kernel_id}", params=self.params | 
					
						
							| 
									
										
										
										
											2025-03-04 12:16:40 +08:00
										 |  |  |                 ) as response: | 
					
						
							| 
									
										
										
										
											2025-03-04 11:59:39 +08:00
										 |  |  |                     response.raise_for_status() | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |             except Exception as err: | 
					
						
							|  |  |  |                 logger.exception("close kernel failed, %s", err) | 
					
						
							|  |  |  |         await self.session.close() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def run(self) -> ResultModel: | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             await self.sign_in() | 
					
						
							|  |  |  |             await self.init_kernel() | 
					
						
							|  |  |  |             await self.execute_code() | 
					
						
							|  |  |  |         except Exception as err: | 
					
						
							| 
									
										
										
										
											2025-03-04 12:01:08 +08:00
										 |  |  |             logger.exception("execute code failed, %s", err) | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |             self.result.stderr = f"Error: {err}" | 
					
						
							|  |  |  |         return self.result | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def sign_in(self) -> None: | 
					
						
							|  |  |  |         # password authentication | 
					
						
							|  |  |  |         if self.password and not self.token: | 
					
						
							| 
									
										
										
										
											2025-05-10 02:32:01 +08:00
										 |  |  |             async with self.session.get("login") as response: | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |                 response.raise_for_status() | 
					
						
							|  |  |  |                 xsrf_token = response.cookies["_xsrf"].value | 
					
						
							|  |  |  |                 if not xsrf_token: | 
					
						
							|  |  |  |                     raise ValueError("_xsrf token not found") | 
					
						
							|  |  |  |                 self.session.cookie_jar.update_cookies(response.cookies) | 
					
						
							|  |  |  |                 self.session.headers.update({"X-XSRFToken": xsrf_token}) | 
					
						
							|  |  |  |             async with self.session.post( | 
					
						
							| 
									
										
										
										
											2025-05-10 02:32:01 +08:00
										 |  |  |                 "login", | 
					
						
							| 
									
										
										
										
											2025-03-04 12:16:40 +08:00
										 |  |  |                 data={"_xsrf": xsrf_token, "password": self.password}, | 
					
						
							|  |  |  |                 allow_redirects=False, | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |             ) as response: | 
					
						
							|  |  |  |                 response.raise_for_status() | 
					
						
							|  |  |  |                 self.session.cookie_jar.update_cookies(response.cookies) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # token authentication | 
					
						
							|  |  |  |         if self.token: | 
					
						
							|  |  |  |             self.params.update({"token": self.token}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def init_kernel(self) -> None: | 
					
						
							| 
									
										
										
										
											2025-05-10 23:00:01 +08:00
										 |  |  |         async with self.session.post(url="api/kernels", params=self.params) as response: | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |             response.raise_for_status() | 
					
						
							|  |  |  |             kernel_data = await response.json() | 
					
						
							|  |  |  |             self.kernel_id = kernel_data["id"] | 
					
						
							| 
									
										
										
										
											2025-02-10 18:25:02 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |     def init_ws(self) -> (str, dict): | 
					
						
							| 
									
										
										
										
											2025-05-10 02:32:01 +08:00
										 |  |  |         ws_base = self.base_url.replace("http", "ws", 1) | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |         ws_params = "?" + "&".join([f"{key}={val}" for key, val in self.params.items()]) | 
					
						
							| 
									
										
										
										
											2025-05-10 02:32:01 +08:00
										 |  |  |         websocket_url = f"{ws_base}api/kernels/{self.kernel_id}/channels{ws_params if len(ws_params) > 1 else ''}" | 
					
						
							| 
									
										
										
										
											2025-02-10 18:25:02 +08:00
										 |  |  |         ws_headers = {} | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |         if self.password and not self.token: | 
					
						
							| 
									
										
										
										
											2025-03-03 22:05:50 +08:00
										 |  |  |             ws_headers = { | 
					
						
							| 
									
										
										
										
											2025-03-04 12:16:40 +08:00
										 |  |  |                 "Cookie": "; ".join( | 
					
						
							|  |  |  |                     [ | 
					
						
							|  |  |  |                         f"{cookie.key}={cookie.value}" | 
					
						
							|  |  |  |                         for cookie in self.session.cookie_jar | 
					
						
							|  |  |  |                     ] | 
					
						
							|  |  |  |                 ), | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |                 **self.session.headers, | 
					
						
							| 
									
										
										
										
											2025-03-03 22:05:50 +08:00
										 |  |  |             } | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |         return websocket_url, ws_headers | 
					
						
							| 
									
										
										
										
											2025-02-10 18:25:02 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |     async def execute_code(self) -> None: | 
					
						
							|  |  |  |         # initialize ws | 
					
						
							|  |  |  |         websocket_url, ws_headers = self.init_ws() | 
					
						
							|  |  |  |         # execute | 
					
						
							| 
									
										
										
										
											2025-03-04 12:16:40 +08:00
										 |  |  |         async with websockets.connect( | 
					
						
							|  |  |  |             websocket_url, additional_headers=ws_headers | 
					
						
							|  |  |  |         ) as ws: | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |             await self.execute_in_jupyter(ws) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-04 12:16:40 +08:00
										 |  |  |     async def execute_in_jupyter(self, ws) -> None: | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |         # send message | 
					
						
							|  |  |  |         msg_id = uuid.uuid4().hex | 
					
						
							|  |  |  |         await ws.send( | 
					
						
							|  |  |  |             json.dumps( | 
					
						
							|  |  |  |                 { | 
					
						
							|  |  |  |                     "header": { | 
					
						
							|  |  |  |                         "msg_id": msg_id, | 
					
						
							|  |  |  |                         "msg_type": "execute_request", | 
					
						
							|  |  |  |                         "username": "user", | 
					
						
							|  |  |  |                         "session": uuid.uuid4().hex, | 
					
						
							|  |  |  |                         "date": "", | 
					
						
							|  |  |  |                         "version": "5.3", | 
					
						
							|  |  |  |                     }, | 
					
						
							|  |  |  |                     "parent_header": {}, | 
					
						
							|  |  |  |                     "metadata": {}, | 
					
						
							|  |  |  |                     "content": { | 
					
						
							|  |  |  |                         "code": self.code, | 
					
						
							|  |  |  |                         "silent": False, | 
					
						
							|  |  |  |                         "store_history": True, | 
					
						
							|  |  |  |                         "user_expressions": {}, | 
					
						
							|  |  |  |                         "allow_stdin": False, | 
					
						
							|  |  |  |                         "stop_on_error": True, | 
					
						
							|  |  |  |                     }, | 
					
						
							|  |  |  |                     "channel": "shell", | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2025-03-03 22:05:50 +08:00
										 |  |  |             ) | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |         ) | 
					
						
							|  |  |  |         # parse message | 
					
						
							|  |  |  |         stdout, stderr, result = "", "", [] | 
					
						
							|  |  |  |         while True: | 
					
						
							|  |  |  |             try: | 
					
						
							|  |  |  |                 # wait for message | 
					
						
							|  |  |  |                 message = await asyncio.wait_for(ws.recv(), self.timeout) | 
					
						
							|  |  |  |                 message_data = json.loads(message) | 
					
						
							|  |  |  |                 # msg id not match, skip | 
					
						
							|  |  |  |                 if message_data.get("parent_header", {}).get("msg_id") != msg_id: | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |                 # check message type | 
					
						
							|  |  |  |                 msg_type = message_data.get("msg_type") | 
					
						
							|  |  |  |                 match msg_type: | 
					
						
							|  |  |  |                     case "stream": | 
					
						
							|  |  |  |                         if message_data["content"]["name"] == "stdout": | 
					
						
							|  |  |  |                             stdout += message_data["content"]["text"] | 
					
						
							|  |  |  |                         elif message_data["content"]["name"] == "stderr": | 
					
						
							|  |  |  |                             stderr += message_data["content"]["text"] | 
					
						
							|  |  |  |                     case "execute_result" | "display_data": | 
					
						
							|  |  |  |                         data = message_data["content"]["data"] | 
					
						
							|  |  |  |                         if "image/png" in data: | 
					
						
							|  |  |  |                             result.append(f"data:image/png;base64,{data['image/png']}") | 
					
						
							|  |  |  |                         elif "text/plain" in data: | 
					
						
							|  |  |  |                             result.append(data["text/plain"]) | 
					
						
							|  |  |  |                     case "error": | 
					
						
							|  |  |  |                         stderr += "\n".join(message_data["content"]["traceback"]) | 
					
						
							|  |  |  |                     case "status": | 
					
						
							|  |  |  |                         if message_data["content"]["execution_state"] == "idle": | 
					
						
							|  |  |  |                             break | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             except asyncio.TimeoutError: | 
					
						
							|  |  |  |                 stderr += "\nExecution timed out." | 
					
						
							|  |  |  |                 break | 
					
						
							|  |  |  |         self.result.stdout = stdout.strip() | 
					
						
							|  |  |  |         self.result.stderr = stderr.strip() | 
					
						
							|  |  |  |         self.result.result = "\n".join(result).strip() if result else "" | 
					
						
							| 
									
										
										
										
											2025-02-10 18:25:02 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | async def execute_code_jupyter( | 
					
						
							|  |  |  |     base_url: str, code: str, token: str = "", password: str = "", timeout: int = 60 | 
					
						
							|  |  |  | ) -> dict: | 
					
						
							| 
									
										
										
										
											2025-03-04 12:16:40 +08:00
										 |  |  |     async with JupyterCodeExecuter( | 
					
						
							|  |  |  |         base_url, code, token, password, timeout | 
					
						
							|  |  |  |     ) as executor: | 
					
						
							| 
									
										
										
										
											2025-03-04 11:55:01 +08:00
										 |  |  |         result = await executor.run() | 
					
						
							|  |  |  |         return result.model_dump() |