Skip to content
139 changes: 132 additions & 7 deletions packages/prime/src/prime_cli/commands/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from ..api.inference import InferenceAPIError, InferenceClient
from ..utils import output_data_as_json, validate_output_format
from ..utils.env_metadata import get_environment_metadata
from ..utils.eval_push import push_eval_results_to_hub

app = typer.Typer(help="Manage verifiers environments", no_args_is_help=True)
Expand All @@ -33,6 +34,39 @@
DEFAULT_LIST_LIMIT = 20


def display_remote_environment_info(
env_path: Optional[Path] = None, environment_name: Optional[str] = None
) -> None:
"""Display the remote environment name if metadata exists.

Checks the provided path (or current directory) for environment metadata
and displays "Using remote environment {owner}/{name}" if found.

If environment_name is provided, also checks ./environments/{module_name} as a fallback.

Args:
env_path: Path to check for metadata (defaults to current directory)
environment_name: Optional environment name to check in ./environments/{module_name}
"""
if env_path is None:
env_path = Path.cwd()

# Check the provided path first
env_metadata = get_environment_metadata(env_path)

# If not found and environment_name is provided, check ./environments/{module_name}
if not env_metadata and environment_name:
current_dir = Path.cwd()
module_name = environment_name.replace("-", "_")
env_dir = current_dir / "environments" / module_name
env_metadata = get_environment_metadata(env_dir)

if env_metadata and env_metadata.get("owner") and env_metadata.get("name"):
owner = env_metadata.get("owner")
env_name = env_metadata.get("name")
console.print(f"Using remote environment {owner}/{env_name}\n")


def should_include_file_in_archive(file_path: Path, base_path: Path) -> bool:
"""Determine if a file should be included in the archive based on filtering rules."""
if not file_path.is_file():
Expand All @@ -54,7 +88,7 @@ def should_include_directory_in_archive(dir_path: Path) -> bool:
if not dir_path.is_dir():
return False

# Skip hidden directories
# Skip hidden directories (includes .prime/, .git/, etc.)
if dir_path.name.startswith("."):
return False

Expand Down Expand Up @@ -242,6 +276,9 @@ def push(
try:
env_path = Path(path).resolve()

# Display remote environment info if metadata exists
display_remote_environment_info(env_path)

# Validate basic structure
pyproject_path = env_path / "pyproject.toml"
if not pyproject_path.exists():
Expand Down Expand Up @@ -648,20 +685,71 @@ def push(
console.print(f"Wheel: {wheel_path.name}")
console.print(f"SHA256: {wheel_sha256}")

# Save environment hub metadata for future reference
# Save or update environment hub metadata for future reference
try:
prime_dir = env_path / ".prime"
prime_dir.mkdir(exist_ok=True)
metadata_path = prime_dir / ".env-metadata.json"

# Backwards compatibility: Migrate .env-metadata.json from root to .prime/
# This handles environments that were pulled/pushed before we moved
# to .prime/ subfolder
old_metadata_path = env_path / ".env-metadata.json"
if old_metadata_path.exists() and not metadata_path.exists():
try:
# Move the old file to the new location
old_metadata_path.rename(metadata_path)
console.print(
"[dim]Migrated environment metadata from root "
"to .prime/ subfolder[/dim]"
)
except (OSError, IOError) as e:
console.print(
f"[yellow]Warning: Could not migrate old .env-metadata.json "
f"file to .prime/ subfolder: {e}[/yellow]"
)
elif old_metadata_path.exists() and metadata_path.exists():
# Both exist - prefer the one in .prime/ and remove the old one
try:
old_metadata_path.unlink()
except (OSError, IOError):
console.print(
"[yellow]Warning: Could not remove old .env-metadata.json "
)

# Read existing metadata if it exists
existing_metadata = {}
if metadata_path.exists():
try:
with open(metadata_path, "r") as f:
existing_metadata = json.load(f)
except (json.JSONDecodeError, IOError) as e:
console.print(
f"[yellow]Warning: Could not read existing metadata: {e}[/yellow]"
)
existing_metadata = {}

# Merge existing metadata with new push information
env_metadata = {
**existing_metadata, # Preserve existing fields
"environment_id": env_id,
"owner": owner_name,
"name": env_name,
"pushed_at": datetime.now().isoformat(),
"wheel_sha256": wheel_sha256,
"visibility": visibility,
}
metadata_path = env_path / ".env-metadata.json"

with open(metadata_path, "w") as f:
json.dump(env_metadata, f, indent=2)
console.print(f"[dim]Saved environment metadata to {metadata_path.name}[/dim]")

if existing_metadata:
console.print(
"[dim]Updated environment metadata in .prime/.env-metadata.json[/dim]"
)
else:
console.print(
"[dim]Saved environment metadata to .prime/.env-metadata.json[/dim]"
)
except Exception as e:
console.print(
f"[yellow]Warning: Could not save environment metadata: {e}[/yellow]"
Expand Down Expand Up @@ -782,7 +870,19 @@ def pull(
if target:
target_dir = Path(target)
else:
target_dir = Path.cwd() / f"{owner}-{name}-{version}"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gives it more of a git-like feel, so I prime env pull will/wordle into the wordle dir for development

# Check if the base directory exists and add index suffix if needed
base_dir = Path.cwd() / name
target_dir = base_dir
if target_dir.exists():
# Find the next available directory with index suffix
index = 1
while target_dir.exists():
target_dir = Path.cwd() / f"{name}-{index}"
index += 1
console.print(
f"[yellow]Directory {base_dir} already exists. "
f"Using {target_dir} instead.[/yellow]"
)

try:
target_dir.mkdir(parents=True, exist_ok=True)
Expand Down Expand Up @@ -840,8 +940,30 @@ def pull(

console.print(f"[green]✓ Environment pulled to {target_dir}[/green]")

# Create .env-metadata.json for proper resolution
try:
extracted_files = list(target_dir.iterdir())
prime_dir = target_dir / ".prime"
prime_dir.mkdir(exist_ok=True)
metadata_path = prime_dir / ".env-metadata.json"
env_metadata = {
"environment_id": details.get("id"),
"owner": owner,
"name": name,
}
with open(metadata_path, "w") as f:
json.dump(env_metadata, f, indent=2)
console.print("[dim]Created environment metadata at .prime/.env-metadata.json[/dim]")
except Exception as e:
console.print(f"[yellow]Warning: Could not create metadata file: {e}[/yellow]")

try:
all_files = list(target_dir.iterdir())
# Filter out .prime directory and .env-metadata.json files
# (created locally, not extracted)
extracted_files = [
f for f in all_files
if f.name != ".prime" and f.name != ".env-metadata.json"
]
if extracted_files:
console.print("\nExtracted files:")
for file in extracted_files[:MAX_FILES_TO_SHOW]:
Expand Down Expand Up @@ -1694,6 +1816,9 @@ def eval_env(
prime env eval meow -m meta-llama/llama-3.1-70b-instruct -n 2 -r 3 -t 1024 -T 0.7
All extra args are forwarded unchanged to vf-eval.
"""
# Display remote environment info if metadata exists
display_remote_environment_info(environment_name=environment)

config = Config()

api_key = config.api_key
Expand Down
33 changes: 33 additions & 0 deletions packages/prime/src/prime_cli/utils/env_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Utilities for reading and managing environment metadata."""
import json
from pathlib import Path
from typing import Any, Dict, Optional


def get_environment_metadata(env_path: Path) -> Optional[Dict[str, Any]]:
"""Read environment metadata from .prime/.env-metadata.json with backwards compatibility.

Checks both the new location (.prime/.env-metadata.json) and old location
(.env-metadata.json) for backwards compatibility.

Args:
env_path: Path to the environment directory

Returns:
Dictionary containing environment metadata, or None if not found
"""
# Try new location first
metadata_path = env_path / ".prime" / ".env-metadata.json"
if not metadata_path.exists():
# Fall back to old location for backwards compatibility
metadata_path = env_path / ".env-metadata.json"

if not metadata_path.exists():
return None

try:
with open(metadata_path, "r") as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
return None

19 changes: 8 additions & 11 deletions packages/prime/src/prime_cli/utils/eval_push.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from prime_evals import EvalsAPIError, EvalsClient
from rich.console import Console

from .env_metadata import get_environment_metadata

console = Console()


Expand Down Expand Up @@ -71,19 +73,14 @@ def push_eval_results_to_hub(

console.print(f"[dim]Loaded {len(results_samples)} samples[/dim]")

env_metadata_path = Path("./environments") / module_name / ".env-metadata.json"
# Resolve environment slug from metadata if available
env_dir = Path("./environments") / module_name
hub_metadata = get_environment_metadata(env_dir)
resolved_env_slug = None

if env_metadata_path.exists():
try:
with open(env_metadata_path, encoding="utf-8") as f:
hub_metadata = json.load(f)
if hub_metadata.get("owner") and hub_metadata.get("name"):
resolved_env_slug = f"{hub_metadata.get('owner')}/{hub_metadata.get('name')}"
console.print(f"[blue]✓ Found environment:[/blue] {env_name}")
except (json.JSONDecodeError, IOError, KeyError) as e:
console.print(f"[yellow]Warning: Could not load {env_metadata_path}: {e}[/yellow]")
console.print(f"[blue]Using environment name:[/blue] {env_name}")
if hub_metadata and hub_metadata.get("owner") and hub_metadata.get("name"):
resolved_env_slug = f"{hub_metadata.get('owner')}/{hub_metadata.get('name')}"
console.print(f"[blue]✓ Found environment:[/blue] {env_name}")
else:
console.print(f"[blue]Using environment name:[/blue] {env_name} (will be resolved)")

Expand Down