diff --git a/packages/prime/src/prime_cli/commands/env.py b/packages/prime/src/prime_cli/commands/env.py index 6a90be9c..d3adf495 100644 --- a/packages/prime/src/prime_cli/commands/env.py +++ b/packages/prime/src/prime_cli/commands/env.py @@ -1209,16 +1209,86 @@ def update_pyproject_version(pyproject_path: Path, new_version: str) -> None: f.write(updated_content) -def get_install_command(tool: str, wheel_url: str) -> List[str]: +def get_install_command(tool: str, wheel_url: str, use_system: bool = False) -> List[str]: """Generate install command for the specified tool.""" if tool == "uv": - return ["uv", "pip", "install", "--upgrade", wheel_url] + cmd = ["uv", "pip", "install"] + if use_system: + cmd.append("--system") + cmd.append("--upgrade") + cmd.append(wheel_url) + return cmd elif tool == "pip": return ["pip", "install", "--upgrade", wheel_url] else: raise ValueError(f"Unsupported package manager: {tool}. Use 'uv' or 'pip'.") +def _venv_bin_dir(venv_path: Path) -> Path: + return venv_path / ("Scripts" if os.name == "nt" else "bin") + + +def _build_venv_env(venv_path: Path) -> Dict[str, str]: + env = os.environ.copy() + env["VIRTUAL_ENV"] = str(venv_path) + env["UV_PROJECT_ENVIRONMENT"] = str(venv_path) + bin_dir = _venv_bin_dir(venv_path) + env["PATH"] = f"{bin_dir}{os.pathsep}{env.get('PATH', '')}" + return env + + +def _ensure_uv_venv(venv_path: Path) -> None: + venv_path.parent.mkdir(parents=True, exist_ok=True) + cfg = venv_path / "pyvenv.cfg" + if cfg.exists(): + return + console.print(f"[dim]Creating virtual environment at {venv_path}...[/dim]") + result = subprocess.run( + ["uv", "venv", str(venv_path)], + capture_output=True, + text=True, + ) + if result.returncode != 0: + msg = (result.stdout or result.stderr or "").strip() + raise Exception(f"Failed to create virtual environment at {venv_path}: {msg}") + + +def _find_project_root(start: Path) -> Optional[Path]: + for parent in (start, *start.parents): + if (parent / "pyproject.toml").exists() or (parent / "uv.toml").exists(): + return parent + return None + + +def _resolve_uv_install_context( + install_target: str, project: Optional[Path] +) -> Tuple[bool, Optional[Dict[str, str]], Optional[Path], Optional[Path]]: + install_target = install_target.lower().strip() + if install_target == "system": + return True, None, None, None + + if install_target == "prime": + config = Config() + venv_path = config.config_dir / "venvs" / "default" + _ensure_uv_venv(venv_path) + return False, _build_venv_env(venv_path), None, venv_path + + if install_target == "project": + project_root = project or _find_project_root(Path.cwd()) + if not project_root: + raise ValueError( + "No project directory with pyproject.toml or uv.toml found. " + "Use --project or --install-target prime|system." + ) + venv_path = project_root / ".venv" + _ensure_uv_venv(venv_path) + return False, _build_venv_env(venv_path), project_root, venv_path + + raise ValueError( + "Invalid install target. Use one of: prime, project, system." + ) + + @app.command(no_args_is_help=True) def info( env_id: str = typer.Argument(..., help="Environment ID (owner/name)"), @@ -1373,7 +1443,14 @@ def process_wheel_url(wheel_url: Optional[str]) -> Optional[str]: return wheel_url -def execute_install_command(cmd: List[str], env_id: str, version: str, tool: str) -> None: +def execute_install_command( + cmd: List[str], + env_id: str, + version: str, + tool: str, + env: Optional[Dict[str, str]] = None, + cwd: Optional[Path] = None, +) -> None: """Execute the installation command with proper output handling. Args: @@ -1395,6 +1472,8 @@ def execute_install_command(cmd: List[str], env_id: str, version: str, tool: str text=True, bufsize=1, universal_newlines=True, + env=env, + cwd=str(cwd) if cwd else None, ) # Stream output line by line @@ -1420,6 +1499,19 @@ def install( "--with", help="Package manager to use (uv or pip)", ), + install_target: str = typer.Option( + "prime", + "--install-target", + help=( + "Where to install environments when using uv: prime (managed), " + "project (.venv), or system" + ), + ), + project: Optional[Path] = typer.Option( + None, + "--project", + help="Project directory to use with --install-target project", + ), ) -> None: """Install a verifiers environment @@ -1447,6 +1539,40 @@ def install( # De-dup environment IDs just in case env_ids = list(dict.fromkeys(env_ids)) + install_target = install_target.lower().strip() + + use_system = False + env_overrides: Optional[Dict[str, str]] = None + install_cwd: Optional[Path] = None + venv_path: Optional[Path] = None + + if with_tool == "uv": + if install_target not in ["prime", "project", "system"]: + console.print( + "[red]Invalid install target. Use one of: prime, project, system.[/red]" + ) + raise typer.Exit(1) + try: + use_system, env_overrides, install_cwd, venv_path = _resolve_uv_install_context( + install_target, project + ) + except ValueError as e: + console.print(f"[red]{e}[/red]") + raise typer.Exit(1) + + if use_system: + console.print( + "[yellow]Using system Python for install (`uv pip install --system`).[/yellow]" + ) + elif venv_path: + label = "Prime managed" if install_target == "prime" else "Project" + console.print(f"[dim]{label} environment: {venv_path}[/dim]") + else: + if project is not None or install_target not in ["prime", "system"]: + console.print( + "[yellow]--install-target/--project only apply to uv; pip will use the current Python environment.[/yellow]" + ) + # Resolving and validating environments installable_envs = [] failed_envs = [] @@ -1507,7 +1633,7 @@ def install( console.print(f"[green]✓ Found {env_id}@{target_version}[/green]") cmd_parts = _build_install_command( - name, target_version, simple_index_url, wheel_url, with_tool + name, target_version, simple_index_url, wheel_url, with_tool, use_system=use_system ) if not cmd_parts: skipped_envs.append((f"{env_id}@{target_version}", "No installation method")) @@ -1532,7 +1658,7 @@ def install( ) for cmd_parts, env_id, target_version, name in installable_envs: try: - execute_install_command(cmd_parts, env_id, target_version, with_tool) + execute_install_command(cmd_parts, env_id, target_version, with_tool, env=env_overrides, cwd=install_cwd) installed_envs.append((env_id, target_version)) # Display usage instructions @@ -1556,6 +1682,11 @@ def install( for env_id, version in installed_envs: console.print(f"[green]✓ {env_id}@{version}[/green]") + if venv_path and with_tool == "uv" and not use_system: + activate_path = _venv_bin_dir(venv_path) / ("activate.bat" if os.name == "nt" else "activate") + console.print(f"\n[dim]Installed into virtual environment: {venv_path}[/dim]") + console.print(f"[dim]Activate with: {activate_path}[/dim]") + if install_failed_envs: console.print( f"\n[bold]Failed to install {len(install_failed_envs)} " @@ -1631,6 +1762,19 @@ def uninstall( "--with", help="Package manager to use (uv or pip)", ), + install_target: str = typer.Option( + "prime", + "--install-target", + help=( + "Where to install environments when using uv: prime (managed), " + "project (.venv), or system" + ), + ), + project: Optional[Path] = typer.Option( + None, + "--project", + help="Project directory to use with --install-target project", + ), ) -> None: """Uninstall a verifiers environment @@ -1879,58 +2023,32 @@ def delete( raise typer.Exit(1) -def _is_environment_installed(env_name: str, required_version: Optional[str] = None) -> bool: - """Check if an environment package is installed.""" - try: - pkg_name = normalize_package_name(env_name) - result = subprocess.run( - ["uv", "pip", "show", pkg_name], - capture_output=True, - text=True, - ) - - if result.returncode != 0: - return False - - if required_version and required_version != "latest": - for line in result.stdout.splitlines(): - if line.startswith("Version:"): - installed_version = line.split(":", 1)[1].strip() - return installed_version == required_version - return False - - return True - except Exception: - return False - - def _build_install_command( name: str, version: str, simple_index_url: Optional[str], wheel_url: Optional[str], tool: str = "uv", + use_system: bool = False, ) -> Optional[List[str]]: """Build install command for an environment. Returns None if no install method available.""" normalized_name = normalize_package_name(name) if simple_index_url: if tool == "uv": + base_cmd = ["uv", "pip", "install"] + if use_system: + base_cmd.append("--system") + base_cmd.append("--upgrade") if version and version != "latest": return [ - "uv", - "pip", - "install", - "--upgrade", + *base_cmd, f"{normalized_name}=={version}", "--extra-index-url", simple_index_url, ] return [ - "uv", - "pip", - "install", - "--upgrade", + *base_cmd, normalized_name, "--extra-index-url", simple_index_url, @@ -1955,14 +2073,20 @@ def _build_install_command( ] elif wheel_url: try: - return get_install_command(tool, wheel_url) + return get_install_command(tool, wheel_url, use_system=use_system) except ValueError: return None return None -def _install_single_environment(env_slug: str, tool: str = "uv") -> bool: +def _install_single_environment( + env_slug: str, + tool: str = "uv", + use_system: bool = False, + env: Optional[Dict[str, str]] = None, + cwd: Optional[Path] = None, +) -> bool: """Install a single environment from the hub. Returns True on success.""" try: env_id, version = validate_env_id(env_slug) @@ -1992,19 +2116,58 @@ def _install_single_environment(env_slug: str, tool: str = "uv") -> bool: console.print(f"[red]No installation method available for {env_slug}[/red]") return False - cmd_parts = _build_install_command(name, version, simple_index_url, wheel_url, tool) + cmd_parts = _build_install_command( + name, version, simple_index_url, wheel_url, tool, use_system=use_system + ) if not cmd_parts: console.print(f"[red]Failed to build install command for {env_slug}[/red]") return False try: - execute_install_command(cmd_parts, env_id, version, tool) + execute_install_command(cmd_parts, env_id, version, tool, env=env, cwd=cwd) return True except Exception as e: console.print(f"[red]Installation failed: {e}[/red]") return False +def _is_environment_installed( + env_name: str, + required_version: Optional[str] = None, + use_system: bool = False, + env: Optional[Dict[str, str]] = None, + cwd: Optional[Path] = None, +) -> bool: + """Check if an environment package is installed.""" + try: + pkg_name = normalize_package_name(env_name) + cmd = ["uv", "pip", "show"] + if use_system: + cmd.append("--system") + cmd.append(pkg_name) + result = subprocess.run( + cmd, + capture_output=True, + text=True, + env=env, + cwd=str(cwd) if cwd else None, + ) + + if result.returncode != 0: + return False + + if required_version and required_version != "latest": + for line in result.stdout.splitlines(): + if line.startswith("Version:"): + installed_version = line.split(":", 1)[1].strip() + return installed_version == required_version + return False + + return True + except Exception: + return False + + def run_eval( environment: str, model: str, @@ -2024,6 +2187,8 @@ def run_eval( api_base_url: Optional[str], skip_upload: bool, env_path: Optional[str], + install_target: str = "prime", + project: Optional[Path] = None, ) -> None: """ Run verifiers' vf-eval with Prime Inference @@ -2036,6 +2201,24 @@ def run_eval( upstream_name = None env_name_for_vf_eval = environment + install_target = install_target.lower().strip() + if install_target not in ["prime", "project", "system"]: + console.print( + "[red]Invalid install target. Use one of: prime, project, system.[/red]" + ) + raise typer.Exit(1) + + use_system, uv_env, uv_cwd, venv_path = _resolve_uv_install_context( + install_target, project + ) + if use_system: + console.print( + "[yellow]Using system Python for environment discovery/installation.[/yellow]" + ) + elif venv_path: + label = "Prime managed" if install_target == "prime" else "Project" + console.print(f"[dim]{label} environment: {venv_path}[/dim]") + if is_slug: env_slug = environment requested_version = "latest" @@ -2050,9 +2233,20 @@ def run_eval( f"[dim]Using upstream environment {upstream_owner}/{upstream_name}[/dim]\n" ) - if not _is_environment_installed(upstream_name, requested_version): + if not _is_environment_installed( + upstream_name, + requested_version, + use_system=use_system, + env=uv_env, + cwd=uv_cwd, + ): console.print(f"[cyan]Installing {environment}...[/cyan]") - if not _install_single_environment(environment): + if not _install_single_environment( + environment, + use_system=use_system, + env=uv_env, + cwd=uv_cwd, + ): raise typer.Exit(1) console.print() @@ -2110,7 +2304,10 @@ def run_eval( ) raise typer.Exit(1) - cmd = ["uv", "run", "vf-eval", env_name_for_vf_eval] + cmd = ["uv", "run"] + if uv_env is not None and not use_system: + cmd.append("--active") + cmd += ["vf-eval", env_name_for_vf_eval] # Add chosen inference url cmd += ["-b", inference_url] @@ -2119,7 +2316,7 @@ def run_eval( cmd += ["-m", model] # Environment modification may be necessary for passing in API key - env = os.environ.copy() + env = (uv_env.copy() if uv_env is not None else os.environ.copy()) # API key var: respect --api-key-var if provided to this command, else inject PRIME_API_KEY if api_key_var: @@ -2172,7 +2369,7 @@ def run_eval( # Execute; stream output directly try: - result = subprocess.run(cmd, env=env) + result = subprocess.run(cmd, env=env, cwd=str(uv_cwd) if uv_cwd else None) if result.returncode != 0: raise typer.Exit(result.returncode) except KeyboardInterrupt: diff --git a/packages/prime/src/prime_cli/commands/evals.py b/packages/prime/src/prime_cli/commands/evals.py index e6761273..4de57d8c 100644 --- a/packages/prime/src/prime_cli/commands/evals.py +++ b/packages/prime/src/prime_cli/commands/evals.py @@ -582,6 +582,19 @@ def run_eval_cmd( "(used to locate .prime/.env-metadata.json for upstream resolution)" ), ), + install_target: str = typer.Option( + "prime", + "--install-target", + help=( + "Where to install environments with uv: prime (managed), " + "project (.venv), or system" + ), + ), + project: Optional[Path] = typer.Option( + None, + "--project", + help="Project directory to use with --install-target project", + ), ) -> None: """ Run verifiers' vf-eval with Prime Inference. @@ -609,4 +622,6 @@ def run_eval_cmd( api_base_url=api_base_url, skip_upload=skip_upload, env_path=env_path, + install_target=install_target, + project=project, )