#!/usr/bin/env python3 """ Export code blocks from Open WebUI chat conversations to local disk. Usage: # Export specific chat python webui-export.py --chat-id --output-dir ./output # Export all recent chats python webui-export.py --all --output-dir ./output # Watch for new messages and auto-export python webui-export.py --watch --output-dir ./output """ import argparse import json import os import re import time from datetime import datetime from pathlib import Path from typing import Dict, List, Optional import requests class OpenWebUIExporter: """Export code from Open WebUI chats via REST API.""" def __init__(self, base_url: str = "http://localhost:8080", api_key: Optional[str] = None): """ Initialize exporter. :param base_url: Open WebUI base URL (default: http://localhost:8080) :param api_key: Optional API key for authentication """ self.base_url = base_url.rstrip("/") self.api_url = f"{self.base_url}/api/v1" self.session = requests.Session() if api_key: self.session.headers["Authorization"] = f"Bearer {api_key}" def get_chats(self) -> List[Dict]: """Retrieve all user chats.""" response = self.session.get(f"{self.api_url}/chats/all") response.raise_for_status() return response.json() def get_chat(self, chat_id: str) -> Dict: """Retrieve specific chat by ID.""" response = self.session.get(f"{self.api_url}/chats/{chat_id}") response.raise_for_status() return response.json() def extract_code_blocks(self, chat: Dict) -> List[Dict]: """ Extract code blocks from chat messages. :param chat: Chat object with messages :return: List of code blocks with metadata """ code_blocks = [] messages = chat.get("chat", {}).get("messages", []) for idx, message in enumerate(messages): role = message.get("role", "unknown") content = message.get("content", "") # Find code blocks in markdown (```language\ncode\n```) pattern = r"```(\w*)\n(.*?)```" matches = re.findall(pattern, content, re.DOTALL) for match_idx, (language, code) in enumerate(matches): code_blocks.append({ "chat_id": chat.get("id"), "chat_title": chat.get("title", "Untitled"), "message_index": idx, "role": role, "language": language or "txt", "code": code.strip(), "block_index": match_idx, "timestamp": message.get("timestamp", time.time()), }) return code_blocks def save_code_blocks(self, code_blocks: List[Dict], output_dir: Path): """ Save code blocks to disk. :param code_blocks: List of code blocks with metadata :param output_dir: Output directory """ output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) for block in code_blocks: # Create subdirectory per chat chat_dir = output_dir / self._sanitize_filename(block["chat_title"]) chat_dir.mkdir(exist_ok=True) # Determine file extension ext = self._get_extension(block["language"]) # Create filename: message_index-block_index.ext filename = f"{block['message_index']:03d}-{block['block_index']:02d}{ext}" filepath = chat_dir / filename # Save code with open(filepath, "w") as f: f.write(block["code"]) # Save metadata meta_filepath = filepath.with_suffix(filepath.suffix + ".meta.json") with open(meta_filepath, "w") as f: json.dump({ "chat_id": block["chat_id"], "chat_title": block["chat_title"], "message_index": block["message_index"], "role": block["role"], "language": block["language"], "timestamp": block["timestamp"], "timestamp_human": datetime.fromtimestamp(block["timestamp"]).isoformat(), }, f, indent=2) print(f"✓ Saved: {filepath}") def export_chat(self, chat_id: str, output_dir: Path): """Export code blocks from specific chat.""" print(f"Fetching chat {chat_id}...") chat = self.get_chat(chat_id) code_blocks = self.extract_code_blocks(chat) print(f"Found {len(code_blocks)} code blocks") if code_blocks: self.save_code_blocks(code_blocks, output_dir) print(f"✓ Exported {len(code_blocks)} code blocks to {output_dir}") else: print("No code blocks found in chat") def export_all_chats(self, output_dir: Path): """Export code blocks from all chats.""" print("Fetching all chats...") chats = self.get_chats() print(f"Found {len(chats)} chats") total_blocks = 0 for chat in chats: code_blocks = self.extract_code_blocks(chat) if code_blocks: self.save_code_blocks(code_blocks, output_dir) total_blocks += len(code_blocks) print(f"✓ Exported {total_blocks} code blocks from {len(chats)} chats to {output_dir}") @staticmethod def _sanitize_filename(name: str) -> str: """Sanitize filename by removing invalid characters.""" # Remove or replace invalid characters name = re.sub(r'[<>:"/\\|?*]', '_', name) # Limit length return name[:100] @staticmethod def _get_extension(language: str) -> str: """Get file extension for language.""" extensions = { "python": ".py", "javascript": ".js", "typescript": ".ts", "java": ".java", "c": ".c", "cpp": ".cpp", "csharp": ".cs", "go": ".go", "rust": ".rs", "ruby": ".rb", "php": ".php", "swift": ".swift", "kotlin": ".kt", "bash": ".sh", "shell": ".sh", "sql": ".sql", "html": ".html", "css": ".css", "json": ".json", "yaml": ".yaml", "yml": ".yml", "xml": ".xml", "markdown": ".md", "md": ".md", } return extensions.get(language.lower(), ".txt") def main(): parser = argparse.ArgumentParser( description="Export code blocks from Open WebUI chats" ) parser.add_argument( "--base-url", default="http://localhost:8080", help="Open WebUI base URL (default: http://localhost:8080)", ) parser.add_argument( "--api-key", help="Optional API key for authentication", ) parser.add_argument( "--chat-id", help="Export specific chat by ID", ) parser.add_argument( "--all", action="store_true", help="Export all chats", ) parser.add_argument( "--output-dir", default="./webui-exports", help="Output directory (default: ./webui-exports)", ) args = parser.parse_args() exporter = OpenWebUIExporter(base_url=args.base_url, api_key=args.api_key) output_dir = Path(args.output_dir) if args.chat_id: exporter.export_chat(args.chat_id, output_dir) elif args.all: exporter.export_all_chats(output_dir) else: parser.print_help() print("\nError: Specify either --chat-id or --all") return 1 return 0 if __name__ == "__main__": exit(main())