#!/usr/bin/env python # /// script # dependencies = [ # "tomli", # "click", # "inquirer", # ] # [tool.uv] # exclude-newer = "2025-03-07T00:00:00Z" # /// import os import tomli import glob import click import inquirer from pathlib import Path from typing import Dict, Any # For I18N support, we use a simple class to store translations and a global instance # to access it. class I18N: # Define supported languages in current install help script SUPPORTED_LANGUAGES = ['en', 'zh'] # The translation dictionary contains a mapping from language code to a dictionary TRANSLATIONS = { 'en': { # Common 'workspace_not_found': "Workspace root not found.", 'cannot_parse': "Cannot parse {}: {}", 'no_extras_defined': "No extras defined", 'no_extras_found': "No workspace or extras found.", 'operation_canceled': "Operation canceled.", 'available_packages': "Available packages: {}", 'copy_command': "Please copy the above command to execute in terminal. For more help, run:", 'finished': "Finished!", # Description of the CLI command 'cli_description': "UV Workspace Extras Helper - Manage optional dependencies in UV workspace", 'list_cmd_description': "List all extras in the workspace", 'install_cmd_description': "Generate installation commands for extras", 'deploy_cmd_description': "Use predefined deployment templates", # Option descriptions 'verbose_option': "Show detailed dependency information", 'interactive_option': "Interactive guide to generate installation commands", 'all_option': "Generate command to install all extras", 'china_option': "Use Tsinghua PyPI mirror for faster installation in China", 'preset_option': "Use predefined deployment template", 'list_presets_option': "List all predefined deployment templates", 'language_option': "Specify language (en/zh)", # List command 'extras_in_workspace': "Extras in workspace:\n", 'available_extras': "Available extras:", 'dependencies': "dependencies", # Installation command 'install_all_extras': "# Install all optional features:", 'install_extras_for': "# Install {} feature for {}:", 'package_not_in_workspace': "Error: Package '{}' not in workspace or has no extras defined.", 'package_no_extras': "Package '{}' has no extras defined.", 'extra_not_in_package': "Error: Extra '{}' not found in package '{}'.", 'available_extras_in_package': "Available extras: {}", # Interactive installation 'welcome': "Welcome to DB-GPT Installation Assistant!", 'help_message': "This tool will help you generate the correct installation commands.\n", 'select_mode': "Please select installation mode", 'select_extras': "Please select extras to install (space to select/deselect, enter to confirm)", 'installation_info': "📋 Installation Information", 'selected_mode': "📦 Selected mode: {}", 'description': "📝 Description: {}", 'note': "ℹ️ Note: {}", 'will_install': "🧩 Will install the following extras: {}", 'config_file': "⚙️ Configuration file: {}", 'generate_command': "Generate installation command?", 'installation_command': "🚀 Installation Command", 'startup_command': "🏃 Startup Command", 'further_configuration': "⚠️ Further Configuration", 'set_api_key': "Please make sure you set the correct API Key in the configuration file {}", 'set_model_path': "Please make sure you set the correct model path in the configuration file {}", # Deployment command 'available_presets': "Available deployment presets:", 'specify_preset': "Please specify a deployment preset name, or use --list to view all presets", 'preset_not_found': "Error: Preset '{}' not found", 'available_presets_list': "Available presets: {}", 'using_preset': "Using preset '{}' to generate deployment command", # Preset descriptions 'openai_preset': "OpenAI Proxy Mode", 'openai_desc': "Using OpenAI API as proxy, suitable for environments without GPU", 'openai_note': "Requires OpenAI API Key", 'deepseek_preset': "DeepSeek Proxy Mode", 'deepseek_desc': "Using DeepSeek API as proxy, suitable for environments without GPU", 'deepseek_note': "Requires DeepSeek API Key", 'glm4_preset': "GLM4 Local Mode", 'glm4_desc': "Using local GLM4 model, requires GPU environment", 'glm4_note': "Requires local model path configuration", 'vllm_preset': "VLLM Local Mode", 'vllm_desc': "Using VLLM framework to load local model, requires GPU environment", 'vllm_note': "Requires local model path configuration", 'llama_cpp_preset': "LLAMA_CPP Local Mode", 'llama_cpp_desc': "Using LLAMA.cpp framework to load local model, can run on CPU but GPU recommended", 'llama_cpp_note': "Requires local model path configuration, for CUDA support set CMAKE_ARGS=\"-DGGML_CUDA=ON\"", 'ollama_preset': "Ollama Proxy Mode", 'ollama_desc': "Using Ollama as proxy, suitable for environments without GPU", 'ollama_note': "Requires Ollama API Base", 'custom_preset': "Custom Mode", 'custom_desc': "Manually select needed extras", 'custom_note': "Suitable for advanced users" }, 'zh': { # Common 'workspace_not_found': "未找到工作区根目录", 'cannot_parse': "无法解析 {}: {}", 'no_extras_defined': "没有定义 extras", 'no_extras_found': "未找到工作区或没有可选依赖。", 'operation_canceled': "操作已取消。", 'available_packages': "可用的包: {}", 'copy_command': "请复制上面的命令到终端执行。如需更多帮助,请运行:", 'finished': "完成!", # Description of the CLI command 'cli_description': "UV Workspace Extras Helper - 管理UV工作区的可选依赖", 'list_cmd_description': "列出工作区中的所有extras", 'install_cmd_description': "生成安装extras的命令", 'deploy_cmd_description': "使用预设的部署方案", # Option descriptions 'verbose_option': "显示详细依赖信息", 'interactive_option': "交互式引导生成安装命令", 'all_option': "生成安装所有extras的命令", 'china_option': "使用清华pip镜像源加速安装", 'preset_option': "使用预设的部署方案", 'list_presets_option': "列出所有预设部署方案", 'language_option': "指定语言 (en/zh)", # List command 'extras_in_workspace': "工作区中的可选依赖 (extras):\n", 'available_extras': "可用的 extras:", 'dependencies': "个依赖", # Installation command 'install_all_extras': "# 安装所有可选功能:", 'install_extras_for': "# 安装 {} 的 {} 功能:", 'package_not_in_workspace': "错误: 包 '{}' 不在工作区中或没有定义extras。", 'package_no_extras': "包 '{}' 没有定义extras。", 'extra_not_in_package': "错误: 包 '{}' 中没有名为 '{}' 的extra。", 'available_extras_in_package': "可用的extras: {}", # Interactive installation 'welcome': "欢迎使用 DB-GPT 安装引导助手!", 'help_message': "这个工具将帮助你生成正确的安装命令。\n", 'select_mode': "请选择安装模式", 'select_extras': "请选择需要安装的extras(空格选择/取消,回车确认)", 'installation_info': "📋 安装信息", 'selected_mode': "📦 选择的模式: {}", 'description': "📝 描述: {}", 'note': "ℹ️ 注意事项: {}", 'will_install': "🧩 将安装以下extras: {}", 'config_file': "⚙️ 配置文件: {}", 'generate_command': "是否生成安装命令?", 'installation_command': "🚀 安装命令", 'startup_command': "🏃 启动命令", 'further_configuration': "⚠️ 后续配置", 'set_api_key': "请确保在配置文件 {} 中设置了正确的API Key", 'set_model_path': "请确保在配置文件 {} 中设置了正确的模型路径", # Deployment command 'available_presets': "可用的部署预设:", 'specify_preset': "请指定部署预设名称,或使用 --list 查看所有预设", 'preset_not_found': "错误: 未找到预设 '{}'", 'available_presets_list': "可用的预设: {}", 'using_preset': "使用预设 '{}' 生成部署命令", # Preset descriptions 'openai_preset': "OpenAI 代理模式", 'openai_desc': "使用OpenAI API作为代理,适合无GPU环境", 'openai_note': "需要提供OpenAI API Key", 'deepseek_preset': "DeepSeek 代理模式", 'deepseek_desc': "使用DeepSeek API作为代理,适合无GPU环境", 'deepseek_note': "需要提供DeepSeek API Key", 'glm4_preset': "GLM4 本地模式", 'glm4_desc': "使用本地GLM4模型,需要GPU环境", 'glm4_note': "需要配置本地模型路径", 'vllm_preset': "VLLM 本地模式", 'vllm_desc': "使用VLLM框架加载本地模型,需要GPU环境", 'vllm_note': "需要配置本地模型路径", 'llama_cpp_preset': "LLAMA_CPP 本地模式", 'llama_cpp_desc': "使用LLAMA.cpp框架加载本地模型,CPU也可运行但推荐GPU", 'llama_cpp_note': "需要配置本地模型路径,启用CUDA需设置CMAKE_ARGS=\"-DGGML_CUDA=ON\"", 'ollama_preset': "Ollama 代理模式", 'ollama_desc': "使用Ollama作为代理,适合无GPU环境", 'ollama_note': "需要提供Ollama API Base", 'custom_preset': "自定义模式", 'custom_desc': "手动选择需要的extras", 'custom_note': "适合高级用户" } } def __init__(self, lang=None): """Initialize the I18N instance with the specified language""" # If language is not specified, try to get from environment if not lang: try: import locale try: # First try to get the locale from the environment lang = locale.getlocale()[0] except (AttributeError, ValueError): try: lang = locale.getdefaultlocale()[0] except (AttributeError, ValueError): lang = 'en' if lang: lang = lang.split('_')[0] else: lang = 'en' except (ImportError, AttributeError, ValueError): lang = 'en' # If the language is not supported, default to English if lang not in self.SUPPORTED_LANGUAGES: lang = 'en' self.lang = lang def get(self, key): """Get the translation for the specified key""" return self.TRANSLATIONS.get(self.lang, {}).get(key, key) i18n = I18N() def set_language(lang): """Set the global language for the script""" global i18n i18n = I18N(lang) def extract_workspace_extras(): """Determine the workspace root and extract extras dependencies for all packages""" # First locate the workspace root (directory containing pyproject.toml with # tool.uv.workspace) current_dir = os.getcwd() workspace_root = None # Find the workspace root while current_dir != os.path.dirname(current_dir): # Stop at root pyproject_path = os.path.join(current_dir, "pyproject.toml") if os.path.exists(pyproject_path): try: with open(pyproject_path, "rb") as f: pyproject_data = tomli.load(f) if pyproject_data.get("tool", {}).get("uv", {}).get("workspace"): workspace_root = current_dir break except Exception as e: print(i18n.get('cannot_parse').format(pyproject_path, e)) current_dir = os.path.dirname(current_dir) if not workspace_root: print(i18n.get('workspace_not_found')) return {} # Read the workspace configuration with open(os.path.join(workspace_root, "pyproject.toml"), "rb") as f: root_data = tomli.load(f) workspace_config = root_data.get("tool", {}).get("uv", {}).get("workspace", {}) members_patterns = workspace_config.get("members", []) exclude_patterns = workspace_config.get("exclude", []) # Extract all member packages member_dirs = [] for pattern in members_patterns: # Convert glob pattern to absolute path full_pattern = os.path.join(workspace_root, pattern) matches = glob.glob(full_pattern, recursive=True) for match in matches: if os.path.isdir(match) and os.path.exists(os.path.join(match, "pyproject.toml")): # Check if the directory should be excluded should_exclude = False for exclude_pattern in exclude_patterns: if Path(match).match(os.path.join(workspace_root, exclude_pattern)): should_exclude = True break if not should_exclude: member_dirs.append(match) # Add the workspace root as a member package member_dirs.append(workspace_root) # Extract extras for each member package all_extras = {} for member_dir in member_dirs: member_path = os.path.join(member_dir, "pyproject.toml") try: with open(member_path, "rb") as f: member_data = tomli.load(f) project_name = member_data.get("project", {}).get("name", os.path.basename(member_dir)) optional_deps = member_data.get("project", {}).get("optional-dependencies", {}) if optional_deps: all_extras[project_name] = { "path": member_dir, "extras": list(optional_deps.keys()), "details": optional_deps } except Exception as e: print(i18n.get('cannot_parse').format(member_path, e)) return all_extras # Preset deployment templates def get_deployment_presets(): """Get localized deployment presets""" return { i18n.get('openai_preset'): { "extras": ["base", "proxy_openai", "rag", "storage_chromadb", "dbgpts"], "config": "configs/dbgpt-proxy-openai.toml", "description": i18n.get('openai_desc'), "note": i18n.get('openai_note') }, i18n.get('deepseek_preset'): { "extras": ["base", "proxy_openai", "rag", "storage_chromadb", "dbgpts"], "config": "configs/dbgpt-proxy-deepseek.toml", "description": i18n.get('deepseek_desc'), "note": i18n.get('deepseek_note') }, i18n.get('glm4_preset'): { "extras": ["base", "hf", "cuda121", "rag", "storage_chromadb", "quant_bnb", "dbgpts"], "config": "configs/dbgpt-local-glm.toml", "description": i18n.get('glm4_desc'), "note": i18n.get('glm4_note') }, i18n.get('vllm_preset'): { "extras": ["base", "hf", "cuda121", "vllm", "rag", "storage_chromadb", "quant_bnb", "dbgpts"], "config": "configs/dbgpt-local-vllm.toml", "description": i18n.get('vllm_desc'), "note": i18n.get('vllm_note') }, i18n.get('llama_cpp_preset'): { "extras": ["base", "hf", "cuda121", "llama_cpp", "rag", "storage_chromadb", "quant_bnb", "dbgpts"], "config": "configs/dbgpt-local-llama-cpp.toml", "description": i18n.get('llama_cpp_desc'), "note": i18n.get('llama_cpp_note') }, i18n.get('ollama_preset'): { "extras": ["base", "proxy_ollama", "rag", "storage_chromadb", "dbgpts"], "config": "configs/dbgpt-proxy-ollama.toml", "description": i18n.get('ollama_desc'), "note": i18n.get('ollama_note') }, i18n.get('custom_preset'): { "extras": [], "config": "", "description": i18n.get('custom_desc'), "note": i18n.get('custom_note') } } @click.group() @click.option('--language', '-l', type=click.Choice(['en', 'zh']), help=I18N().get('language_option')) def cli(language): """UV Workspace Extras Helper - Manage optional dependencies in UV workspace""" if language: set_language(language) # Update command descriptions to the current language cli.help = i18n.get('cli_description') list_extras.help = i18n.get('list_cmd_description') install_command.help = i18n.get('install_cmd_description') deploy_preset.help = i18n.get('deploy_cmd_description') @cli.command('list') @click.option('--verbose', '-v', is_flag=True, help=i18n.get('verbose_option')) def list_extras(verbose): """List all extras in the workspace""" extras = extract_workspace_extras() if not extras: click.echo(i18n.get('no_extras_found')) return click.echo(i18n.get('extras_in_workspace')) for package, info in extras.items(): click.echo(click.style(f"📦 {package}", fg="green") + click.style(f" ({os.path.relpath(info['path'])})", fg="blue")) if info['extras']: click.echo(f" {i18n.get('available_extras')}") for extra in info['extras']: deps = info['details'][extra] click.echo(f" - {click.style(extra, fg='yellow')}: {len(deps)} {i18n.get('dependencies')}") if verbose: for dep in deps: click.echo(f" • {dep}") else: click.echo(f" {i18n.get('no_extras_defined')}") click.echo() @cli.command('install-cmd') @click.option('--interactive', '-i', is_flag=True, help=i18n.get('interactive_option')) @click.option('--all', 'install_all', is_flag=True, help=i18n.get('all_option')) @click.option('--china', is_flag=True, help=i18n.get('china_option')) @click.argument('package', required=False) @click.argument('extra', required=False) def install_command(interactive, install_all, china, package, extra): """Generate installation commands for extras""" extras = extract_workspace_extras() if not extras: click.echo(i18n.get('no_extras_found')) return # Interactive mode if interactive: _interactive_install_guide(extras, china) return # Install all extras if install_all: all_extras = [] for pkg_info in extras.values(): all_extras.extend(pkg_info['extras']) if all_extras: cmd = "uv sync --all-packages " + " ".join([f"--extra \"{e}\"" for e in all_extras]) if china: cmd += " --index-url=https://pypi.tuna.tsinghua.edu.cn/simple" click.echo(i18n.get('install_all_extras')) click.echo(cmd) else: click.echo(i18n.get('no_extras_found')) return # If no package or extra is provided, show all possible installation commands if not package: for pkg, info in extras.items(): if info['extras']: for e in info['extras']: cmd = f"uv sync --extra \"{e}\"" if china: cmd += " --index-url=https://pypi.tuna.tsinghua.edu.cn/simple" click.echo(i18n.get('install_extras_for').format(pkg, e)) click.echo(cmd) click.echo() return # Check if the specified package exists if package not in extras: click.echo(i18n.get('package_not_in_workspace').format(package)) click.echo(i18n.get('available_packages').format(', '.join(extras.keys()))) return # If no extra is provided, show all extras for the package if not extra: pkg_extras = extras[package]['extras'] if not pkg_extras: click.echo(i18n.get('package_no_extras').format(package)) return cmd = "uv sync " + " ".join([f"--extra \"{e}\"" for e in pkg_extras]) if china: cmd += " --index-url=https://pypi.tuna.tsinghua.edu.cn/simple" click.echo(i18n.get('install_extras_for').format(package, ' '.join(pkg_extras))) click.echo(cmd) return # Check if the specified extra exists if extra not in extras[package]['extras']: click.echo(i18n.get('extra_not_in_package').format(extra, package)) click.echo(i18n.get('available_extras_in_package').format(', '.join(extras[package]['extras']))) return # Show the command to install the specified extra cmd = f"uv sync --extra \"{extra}\"" if china: cmd += " --index-url=https://pypi.tuna.tsinghua.edu.cn/simple" click.echo(i18n.get('install_extras_for').format(package, extra)) click.echo(cmd) def _interactive_install_guide(extras: Dict[str, Any], use_china_mirror: bool = False): """Interactive installation guide""" click.echo(click.style(i18n.get('welcome'), fg="green", bold=True)) click.echo(i18n.get('help_message')) # Get deployment presets deployment_presets = get_deployment_presets() # First step: select installation mode questions = [ inquirer.List('preset', message=i18n.get('select_mode'), choices=[(f"{name} - {info['description']}", name) for name, info in deployment_presets.items()], carousel=True) ] answers = inquirer.prompt(questions) if not answers: return # Operation canceled selected_preset = answers['preset'] preset_info = deployment_presets[selected_preset] # Custom mode: let user select extras if selected_preset == i18n.get('custom_preset'): # Collect all available extras all_available_extras = set() for pkg_info in extras.values(): all_available_extras.update(pkg_info["extras"]) questions = [ inquirer.Checkbox('selected_extras', message=i18n.get('select_extras'), choices=sorted(list(all_available_extras)), carousel=True) ] answers = inquirer.prompt(questions) if not answers or not answers['selected_extras']: click.echo(i18n.get('operation_canceled')) return preset_info['extras'] = answers['selected_extras'] # Show installation information click.echo("\n" + click.style(i18n.get('installation_info'), fg="blue", bold=True)) click.echo(f"{i18n.get('selected_mode')} {click.style(selected_preset, fg='green')}") click.echo(f"{i18n.get('description')} {preset_info['description']}") click.echo(f"{i18n.get('note')} {preset_info['note']}") click.echo(f"{i18n.get('will_install')} {', '.join(preset_info['extras'])}") if preset_info['config']: click.echo(f"{i18n.get('config_file')} {preset_info['config']}") # Confirm installation questions = [ inquirer.Confirm('confirm', message=i18n.get('generate_command'), default=True) ] answers = inquirer.prompt(questions) if not answers or not answers['confirm']: click.echo(i18n.get('operation_canceled')) return # Create installation command cmd = "uv sync --all-packages " + " ".join([f"--extra \"{e}\"" for e in preset_info['extras']]) if use_china_mirror: cmd += " --index-url=https://pypi.tuna.tsinghua.edu.cn/simple" click.echo("\n" + click.style(i18n.get('installation_command'), fg="green", bold=True)) click.echo(cmd) if preset_info.get('config'): click.echo("\n" + click.style(i18n.get('startup_command'), fg="green", bold=True)) click.echo(f"uv run dbgpt start webserver --config {preset_info['config']}") # The step to configure the API key or model path if i18n.get('openai_note') in preset_info['note'] or i18n.get('deepseek_note') in preset_info['note']: click.echo("\n" + click.style(i18n.get('further_configuration'), fg="yellow", bold=True)) if i18n.get('openai_note') in preset_info['note'] or i18n.get('deepseek_note') in preset_info['note']: click.echo(i18n.get('set_api_key').format(preset_info['config'])) elif i18n.get('glm4_note') in preset_info['note'] or i18n.get('vllm_note') in preset_info['note'] or i18n.get('llama_cpp_note') in preset_info['note']: click.echo("\n" + click.style(i18n.get('further_configuration'), fg="yellow", bold=True)) if i18n.get('glm4_note') in preset_info['note'] or i18n.get('vllm_note') in preset_info['note'] or i18n.get('llama_cpp_note') in preset_info['note']: click.echo(i18n.get('set_model_path').format(preset_info['config'])) click.echo("\n" + click.style(f"🎉 {i18n.get('finished')}", fg="green", bold=True)) click.echo(i18n.get('copy_command')) click.echo("uv run install_help.py --help") @cli.command('deploy') @click.option('--preset', '-p', help=i18n.get('preset_option')) @click.option('--china', is_flag=True, help=i18n.get('china_option')) @click.option('--list', 'list_presets', is_flag=True, help=i18n.get('list_presets_option')) def deploy_preset(preset, china, list_presets): """Use predefined deployment templates""" deployment_presets = get_deployment_presets() if list_presets: click.echo(click.style(i18n.get('available_presets'), fg="green", bold=True)) for name, info in deployment_presets.items(): click.echo(f"\n{click.style(name, fg='yellow', bold=True)}") click.echo(f"{i18n.get('description')} {info['description']}") click.echo(f"{i18n.get('note')} {info['note']}") click.echo(f"Extras: {', '.join(info['extras'])}") if info['config']: click.echo(f"{i18n.get('config_file')} {info['config']}") return if not preset: click.echo(i18n.get('specify_preset')) return if preset not in deployment_presets: click.echo(i18n.get('preset_not_found').format(preset)) click.echo(i18n.get('available_presets_list').format(', '.join(deployment_presets.keys()))) return preset_info = deployment_presets[preset] click.echo(i18n.get('using_preset').format(preset)) click.echo(f"{i18n.get('description')} {preset_info['description']}") click.echo(f"{i18n.get('note')} {preset_info['note']}") cmd = "uv sync --all-packages " + " ".join([f"--extra \"{e}\"" for e in preset_info['extras']]) if china: cmd += " --index-url=https://pypi.tuna.tsinghua.edu.cn/simple" click.echo("\n" + click.style(i18n.get('installation_command'), fg="green", bold=True)) click.echo(cmd) if preset_info.get('config'): click.echo("\n" + click.style(i18n.get('startup_command'), fg="green", bold=True)) click.echo(f"uv run dbgpt start webserver --config {preset_info['config']}") if __name__ == '__main__': cli()