chore: import upstream snapshot with attribution
Deploy Documentation / deploy (push) Has been cancelled
CPU Test / Test (Utilities, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, latest, Python 3.13) (push) Has been cancelled
Dashboard / Chromatic (push) Has been cancelled
CPU Test / Lint - fast (push) Has been cancelled
CPU Test / Lint - next (push) Has been cancelled
CPU Test / Lint - slow (push) Has been cancelled
CPU Test / Lint - JavaScript (push) Has been cancelled
CPU Test / Build documentation (push) Has been cancelled
CPU Test / Test (AgentOps, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Others, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Store, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Weave, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (AgentOps, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (LLM proxy, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Others, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Store, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Utilities, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (JavaScript) (push) Has been cancelled
Deploy Documentation / deploy (push) Has been cancelled
CPU Test / Test (Utilities, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, latest, Python 3.13) (push) Has been cancelled
Dashboard / Chromatic (push) Has been cancelled
CPU Test / Lint - fast (push) Has been cancelled
CPU Test / Lint - next (push) Has been cancelled
CPU Test / Lint - slow (push) Has been cancelled
CPU Test / Lint - JavaScript (push) Has been cancelled
CPU Test / Build documentation (push) Has been cancelled
CPU Test / Test (AgentOps, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Others, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Store, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Weave, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (AgentOps, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (LLM proxy, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Others, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Store, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Utilities, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (JavaScript) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/**
|
||||
* Aggregates the “latest completed” results of several dependent workflows and fails
|
||||
* this step if any required job/variant is missing or not successful.
|
||||
*
|
||||
* Usage (from actions/github-script@v8):
|
||||
*
|
||||
* const badgeAggregation = require('./scripts/badge_aggregation.js');
|
||||
* const dependencies = [
|
||||
* { workflow: 'examples-calc-x.yml', label: 'calc-x.latest', variants: ['latest'] },
|
||||
* { workflow: 'examples-spider.yml', label: 'spider.latest', variants: ['latest'] },
|
||||
* { workflow: 'examples-apo.yml', label: 'apo.latest', variants: ['latest'] },
|
||||
* { workflow: 'examples-unsloth.yml', label: 'unsloth.latest', variants: ['latest'] },
|
||||
* { workflow: 'tests-full.yml', label: 'tests-full.latest', variants: ['latest'] },
|
||||
* ];
|
||||
* await badgeAggregation({ github, context, core, dependencies });
|
||||
*
|
||||
* Notes:
|
||||
* - Requires the workflow files above to exist in .github/workflows/.
|
||||
* - Looks at the default branch "main" unless you override per dependency with dep.branch.
|
||||
* - Assumes matrix job names contain the variant in parentheses, e.g. "tests (latest)".
|
||||
*/
|
||||
module.exports = async function badgeAggregation({ github, context, core, dependencies }) {
|
||||
const failures = [];
|
||||
|
||||
// Defensive: validate inputs early for nicer error messages.
|
||||
if (!github?.rest?.actions || !context?.repo || !core) {
|
||||
throw new Error('badgeAggregation: expected { github, context, core } from actions/github-script.');
|
||||
}
|
||||
if (!Array.isArray(dependencies) || dependencies.length === 0) {
|
||||
core.info('No dependencies provided; nothing to check.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Helper: paginate jobs for a run attempt (handles >100 jobs edge case).
|
||||
async function listAllJobsForAttempt(run_id, attempt_number) {
|
||||
const all = [];
|
||||
let page = 1;
|
||||
while (true) {
|
||||
const { data } = await github.rest.actions.listJobsForWorkflowRunAttempt({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id,
|
||||
attempt_number,
|
||||
per_page: 100,
|
||||
page,
|
||||
});
|
||||
const jobs = data.jobs ?? [];
|
||||
all.push(...jobs);
|
||||
if (!data.total_count || all.length >= data.total_count || jobs.length === 0) break;
|
||||
page += 1;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
// For each dependency: find the latest completed run on the target branch; then inspect its jobs.
|
||||
for (const dep of dependencies) {
|
||||
const branch = dep.branch || 'main';
|
||||
|
||||
// You can pass the workflow file name as workflow_id (e.g. "examples-apo.yml").
|
||||
const { data: runsData } = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: dep.workflow,
|
||||
branch: 'main', // Always check the main branch status no matter what
|
||||
status: 'completed', // only completed runs
|
||||
per_page: 50, // retrieve latest 50 so we can filter
|
||||
sort: 'created',
|
||||
direction: 'desc',
|
||||
});
|
||||
|
||||
const filteredRuns = runsData?.workflow_runs?.filter(run => ['schedule', 'workflow_dispatch'].includes(run.event));
|
||||
|
||||
const run = filteredRuns?.[0];
|
||||
if (!run) {
|
||||
failures.push(`No completed run found for ${dep.label} on branch "${branch}"`);
|
||||
continue;
|
||||
}
|
||||
|
||||
core.info(`[${dep.label}] Found run ${run.id} with attempt ${run.run_attempt}`);
|
||||
// Get the specific attempt we want to inspect (latest attempt for that run).
|
||||
const attempt = run.run_attempt ?? 1;
|
||||
|
||||
// Robust: paginate jobs in case the workflow has many.
|
||||
const jobs = await listAllJobsForAttempt(run.id, attempt);
|
||||
core.info(`[${dep.label}] Found ${jobs.length} jobs: ${jobs.map(j => j.name).join(', ')}`);
|
||||
|
||||
// Match each required variant to a job. We look for the variant in parentheses, e.g. "(latest)".
|
||||
for (const variant of dep.variants || []) {
|
||||
const matchingJobs = jobs.filter(
|
||||
j => typeof j.name === 'string' && j.name.includes(variant)
|
||||
);
|
||||
|
||||
if (matchingJobs.length === 0) {
|
||||
failures.push(`Missing job for ${dep.label} (variant: ${variant})`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const job of matchingJobs) {
|
||||
core.info(`[${dep.label}] ${job.name} => ${job.conclusion}`);
|
||||
|
||||
// Accept only a strict "success".
|
||||
if (job.conclusion !== 'success') {
|
||||
failures.push(`${dep.label} (${job.name}) concluded ${job.conclusion}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Surface aggregated result to the workflow.
|
||||
if (failures.length) {
|
||||
core.setFailed(failures.join(' | '));
|
||||
} else {
|
||||
core.info('All latest variants succeeded.');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
FROM nvidia/cuda:12.8.0-cudnn-devel-ubuntu24.04
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
wget \
|
||||
curl \
|
||||
build-essential \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
graphviz \
|
||||
unzip \
|
||||
tmux \
|
||||
vim \
|
||||
git-lfs && \
|
||||
git lfs install && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /workspace
|
||||
Executable
+129
@@ -0,0 +1,129 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Based on: Standard_NC24ads_A100_v4
|
||||
# With: Canonical:ubuntu-24_04-lts:server:latest
|
||||
# Secure boot is off.
|
||||
|
||||
# This script is not designed to be run automatically.
|
||||
|
||||
set -ex
|
||||
|
||||
# Build essentials are required.
|
||||
sudo apt-get clean
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
git \
|
||||
wget \
|
||||
curl \
|
||||
build-essential \
|
||||
software-properties-common \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
graphviz \
|
||||
unzip \
|
||||
tmux \
|
||||
vim \
|
||||
git-lfs \
|
||||
nodejs \
|
||||
gnupg2 \
|
||||
apt-transport-https \
|
||||
ca-certificates \
|
||||
gnupg \
|
||||
lsb-release
|
||||
|
||||
git lfs install
|
||||
|
||||
# VM with GPU needs to install drivers. Reference:
|
||||
# https://docs.microsoft.com/en-us/azure/virtual-machines/linux/n-series-driver-setup
|
||||
sudo apt update && sudo apt install -y ubuntu-drivers-common
|
||||
sudo ubuntu-drivers install
|
||||
sudo reboot now
|
||||
|
||||
# Install CUDA Toolkit
|
||||
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/cuda-keyring_1.1-1_all.deb
|
||||
sudo dpkg -i cuda-keyring_1.1-1_all.deb && rm cuda-keyring_1.1-1_all.deb
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install cuda-toolkit
|
||||
sudo reboot now
|
||||
|
||||
# Add paths globally
|
||||
sudo bash -c "cat > /etc/profile.d/cuda.sh" <<'EOF'
|
||||
export PATH=/usr/local/cuda/bin:$PATH
|
||||
export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH
|
||||
EOF
|
||||
sudo chmod +x /etc/profile.d/cuda.sh
|
||||
|
||||
# Add Docker's official GPG key
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||
sudo chmod a+r /etc/apt/keyrings/docker.asc
|
||||
|
||||
# Add the repository to Apt sources:
|
||||
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
|
||||
Types: deb
|
||||
URIs: https://download.docker.com/linux/ubuntu
|
||||
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
|
||||
Components: stable
|
||||
Signed-By: /etc/apt/keyrings/docker.asc
|
||||
EOF
|
||||
|
||||
sudo apt -y update
|
||||
|
||||
# Install the Docker packages
|
||||
sudo apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
|
||||
# Create docker group only if it doesn't exist
|
||||
# sudo groupadd docker
|
||||
|
||||
# Add current user to docker group if not already a member
|
||||
sudo usermod -aG docker "$USER"
|
||||
# A hack to add cloudtest user to docker group as well
|
||||
sudo sed -i '/^docker:/ s/$/,cloudtest/' /etc/group
|
||||
# This shouldn't be run on CI
|
||||
# newgrp docker
|
||||
|
||||
# Install NVIDIA Container Toolkit
|
||||
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
|
||||
&& curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
|
||||
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
|
||||
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
|
||||
|
||||
sudo apt-get update
|
||||
export NVIDIA_CONTAINER_TOOLKIT_VERSION=1.18.0-1
|
||||
sudo apt-get install -y \
|
||||
nvidia-container-toolkit=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
|
||||
nvidia-container-toolkit-base=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
|
||||
libnvidia-container-tools=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
|
||||
libnvidia-container1=${NVIDIA_CONTAINER_TOOLKIT_VERSION}
|
||||
|
||||
# Configure the NVIDIA Container Toolkit
|
||||
sudo nvidia-ctk runtime configure --runtime=docker
|
||||
sudo systemctl restart docker
|
||||
|
||||
# Install Azure CLI
|
||||
curl -sLS https://packages.microsoft.com/keys/microsoft.asc |
|
||||
gpg --dearmor | sudo tee /etc/apt/keyrings/microsoft.gpg > /dev/null
|
||||
sudo chmod go+r /etc/apt/keyrings/microsoft.gpg
|
||||
AZ_DIST=$(lsb_release -cs)
|
||||
echo "Types: deb
|
||||
URIs: https://packages.microsoft.com/repos/azure-cli/
|
||||
Suites: ${AZ_DIST}
|
||||
Components: main
|
||||
Architectures: $(dpkg --print-architecture)
|
||||
Signed-by: /etc/apt/keyrings/microsoft.gpg" | sudo tee /etc/apt/sources.list.d/azure-cli.sources
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y azure-cli
|
||||
|
||||
# Disable the periodical apt-get upgrade.
|
||||
# Sometimes, unattended upgrade blocks apt-get install
|
||||
sudo sed -i -e "s/Update-Package-Lists \"1\"/Update-Package-Lists \"0\"/g" /etc/apt/apt.conf.d/10periodic
|
||||
sudo sed -i -e "s/Update-Package-Lists \"1\"/Update-Package-Lists \"0\"/g" /etc/apt/apt.conf.d/20auto-upgrades
|
||||
sudo sed -i -e "s/Unattended-Upgrade \"1\"/Unattended-Upgrade \"0\"/g" /etc/apt/apt.conf.d/20auto-upgrades
|
||||
sudo systemctl stop apt-daily.timer apt-daily-upgrade.timer
|
||||
sudo systemctl stop apt-daily.service apt-daily-upgrade.service
|
||||
sudo systemctl mask apt-daily.timer apt-daily-upgrade.timer
|
||||
sudo systemctl mask apt-daily.service apt-daily-upgrade.service
|
||||
|
||||
# Deprovision and prepare for generalized image
|
||||
sudo waagent -deprovision+user
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to bump version in pyproject.toml
|
||||
# Usage: ./bump_version.sh <new_version>
|
||||
|
||||
set -e
|
||||
|
||||
# Check if version argument is provided
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Error: No version specified"
|
||||
echo "Usage: $0 <new_version>"
|
||||
echo "Examples: $0 1.2.3, $0 1.2.3a1, $0 1.2.3b2, $0 1.2.3rc1, $0 1.2.3.post1, $0 1.2.3.dev1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NEW_VERSION="$1"
|
||||
|
||||
# Validate version format (Python PEP 440 compliant)
|
||||
if ! echo "$NEW_VERSION" | grep -E '^[0-9]+(\.[0-9]+)*((a|b|rc)[0-9]+)?(\.post[0-9]+)?(\.dev[0-9]+)?$' > /dev/null; then
|
||||
echo "Error: Invalid version format"
|
||||
echo "Version should follow PEP 440 (e.g., 1.2.3, 1.2.3a1, 1.2.3b2, 1.2.3rc1, 1.2.3.post1, 1.2.3.dev1)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the directory where the script is located
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )"
|
||||
PYPROJECT_FILE="$PROJECT_ROOT/pyproject.toml"
|
||||
|
||||
# Check if pyproject.toml exists
|
||||
if [ ! -f "$PYPROJECT_FILE" ]; then
|
||||
echo "Error: pyproject.toml not found at $PYPROJECT_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get current version
|
||||
CURRENT_VERSION=$(grep '^version = ' "$PYPROJECT_FILE" | sed 's/version = "\(.*\)"/\1/')
|
||||
|
||||
if [ -z "$CURRENT_VERSION" ]; then
|
||||
echo "Error: Could not find current version in pyproject.toml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
echo "New version: $NEW_VERSION"
|
||||
|
||||
# Update version in pyproject.toml
|
||||
sed -i.bak "s/^version = \".*\"/version = \"$NEW_VERSION\"/" "$PYPROJECT_FILE"
|
||||
|
||||
# Remove backup file
|
||||
rm -f "$PYPROJECT_FILE.bak"
|
||||
|
||||
echo "Successfully bumped version from $CURRENT_VERSION to $NEW_VERSION"
|
||||
|
||||
# Update __init__.py if it exists with version
|
||||
INIT_FILE="$PROJECT_ROOT/agentlightning/__init__.py"
|
||||
if [ -f "$INIT_FILE" ]; then
|
||||
if grep -q "__version__" "$INIT_FILE"; then
|
||||
sed -i.bak "s/__version__ = \".*\"/__version__ = \"$NEW_VERSION\"/" "$INIT_FILE"
|
||||
rm -f "$INIT_FILE.bak"
|
||||
echo "Updated version in $INIT_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Version bump complete!"
|
||||
@@ -0,0 +1,105 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Ensure tracked source files include the required copyright header."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HEADER_TEXT = "Copyright (c) Microsoft. All rights reserved."
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
COMMENT_PREFIX_BY_SUFFIX: dict[str, str] = {
|
||||
".py": "#",
|
||||
".pyi": "#",
|
||||
".pyw": "#",
|
||||
".js": "//",
|
||||
".jsx": "//",
|
||||
".ts": "//",
|
||||
".tsx": "//",
|
||||
".mjs": "//",
|
||||
".mts": "//",
|
||||
".cjs": "//",
|
||||
".cts": "//",
|
||||
}
|
||||
REQUIRED_HEADER_BY_SUFFIX = {
|
||||
suffix: f"{prefix} {HEADER_TEXT}" if not prefix.endswith(" ") else f"{prefix}{HEADER_TEXT}"
|
||||
for suffix, prefix in COMMENT_PREFIX_BY_SUFFIX.items()
|
||||
}
|
||||
|
||||
|
||||
def iter_source_files() -> list[Path]:
|
||||
"""Return tracked source files matching supported extensions."""
|
||||
if not REQUIRED_HEADER_BY_SUFFIX:
|
||||
return []
|
||||
|
||||
pathspecs = [f"*{suffix}" for suffix in sorted(REQUIRED_HEADER_BY_SUFFIX)]
|
||||
result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"ls-files",
|
||||
"--cached",
|
||||
"--others",
|
||||
"--exclude-standard",
|
||||
"--",
|
||||
*pathspecs,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
cwd=REPO_ROOT,
|
||||
)
|
||||
return [REPO_ROOT / line.strip() for line in result.stdout.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
missing_header: list[str] = []
|
||||
missing_blank_line: list[str] = []
|
||||
|
||||
for file_path in iter_source_files():
|
||||
expected_header = REQUIRED_HEADER_BY_SUFFIX.get(file_path.suffix.lower())
|
||||
if expected_header is None:
|
||||
continue
|
||||
|
||||
if not file_path.exists():
|
||||
continue
|
||||
|
||||
try:
|
||||
with file_path.open("r", encoding="utf-8") as file:
|
||||
first_line = file.readline().rstrip("\r\n")
|
||||
second_line = file.readline()
|
||||
except OSError as exc:
|
||||
print(f"Failed to read {file_path}: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if first_line != expected_header:
|
||||
missing_header.append(str(file_path.relative_to(REPO_ROOT)))
|
||||
continue
|
||||
|
||||
# Second line should be either an EOF or a blank line
|
||||
if second_line and second_line.strip():
|
||||
missing_blank_line.append(str(file_path.relative_to(REPO_ROOT)))
|
||||
|
||||
if missing_header:
|
||||
print("The following files are missing the required copyright header:")
|
||||
for path in missing_header:
|
||||
print(f" - {path}")
|
||||
header_examples = "\n".join(sorted(set(REQUIRED_HEADER_BY_SUFFIX.values())))
|
||||
print(f"Run the appropriate script or add the header manually:\n{header_examples}")
|
||||
|
||||
if missing_blank_line:
|
||||
print("The following files are missing a blank line after the copyright header:")
|
||||
for path in missing_blank_line:
|
||||
print(f" - {path}")
|
||||
print("Ensure there is an empty line separating the header from the rest of the file.")
|
||||
|
||||
if missing_header or missing_blank_line:
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
set -ex
|
||||
|
||||
rm -rf examples/spider/checkpoints
|
||||
rm -rf examples/calc_x/checkpoints
|
||||
rm -rf examples/unsloth/models
|
||||
rm -rf verl
|
||||
@@ -0,0 +1,87 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import os
|
||||
|
||||
import requests
|
||||
from openai import OpenAI
|
||||
|
||||
# Most common Azure OpenAI setup:
|
||||
# AZURE_OPENAI_ENDPOINT="https://<resource>.openai.azure.com"
|
||||
# AZURE_OPENAI_API_KEY="..."
|
||||
# Optional (only if your endpoint requires it):
|
||||
# AZURE_OPENAI_API_VERSION="2025-xx-xx"
|
||||
#
|
||||
# This script treats "delete finetune job" as "cancel finetune job"
|
||||
# because fine-tune jobs are typically cancellable, not deletable.
|
||||
|
||||
|
||||
def _client() -> OpenAI:
|
||||
# This script assumes AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY are set in the environment.
|
||||
endpoint = os.environ["AZURE_OPENAI_ENDPOINT"]
|
||||
api_key = os.environ["AZURE_OPENAI_API_KEY"]
|
||||
return OpenAI(api_key=api_key, base_url=endpoint)
|
||||
|
||||
|
||||
def list_data_files():
|
||||
c = _client()
|
||||
return c.files.list(limit=100)
|
||||
|
||||
|
||||
def list_finetune_jobs():
|
||||
c = _client()
|
||||
return c.fine_tuning.jobs.list(limit=100)
|
||||
|
||||
|
||||
def delete_data_file(file_id: str):
|
||||
c = _client()
|
||||
return c.files.delete(file_id)
|
||||
|
||||
|
||||
def cancel_finetune_job(job_id: str):
|
||||
c = _client()
|
||||
return c.fine_tuning.jobs.cancel(job_id)
|
||||
|
||||
|
||||
def delete_finetune_job(job_id: str):
|
||||
# This script assumes AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY are set in the environment.
|
||||
endpoint = os.environ["AZURE_OPENAI_ENDPOINT"].rstrip("/")
|
||||
api_key = os.environ["AZURE_OPENAI_API_KEY"]
|
||||
root = endpoint.split("/openai")[0]
|
||||
|
||||
url = f"{root}/openai/fine_tuning/jobs/{job_id}"
|
||||
params = {"api-version": os.environ["AZURE_OPENAI_API_VERSION"]}
|
||||
|
||||
resp = requests.delete(url, headers={"api-key": api_key}, params=params, timeout=60)
|
||||
resp.raise_for_status()
|
||||
return resp.content
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Quick demo: print IDs you could delete
|
||||
jobs = list_finetune_jobs().data
|
||||
files = list_data_files().data
|
||||
|
||||
print("JOBS:")
|
||||
for j in jobs:
|
||||
print(f" {j.id} {getattr(j, 'status', '')} {getattr(j, 'model', '')}")
|
||||
|
||||
print("\nFILES:")
|
||||
for f in files:
|
||||
print(f" {f.id} {getattr(f, 'filename', '')} {getattr(f, 'status', '')}")
|
||||
|
||||
# Delete them all WITHOUT CONFIRMATION!
|
||||
for j in jobs:
|
||||
print(f"Deleting job {j.id}")
|
||||
try:
|
||||
if j.status == "running":
|
||||
cancel_finetune_job(j.id)
|
||||
delete_finetune_job(j.id)
|
||||
except Exception as exc:
|
||||
print(f" Error deleting job {j.id}: {exc}")
|
||||
|
||||
for f in files:
|
||||
print(f"Deleting file {f.id}")
|
||||
try:
|
||||
delete_data_file(f.id)
|
||||
except Exception as exc:
|
||||
print(f" Error deleting file {f.id}: {exc}")
|
||||
@@ -0,0 +1,26 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Generate OpenAPI specification for the LightningStore server.
|
||||
|
||||
Run this every time when you make changes to the LightningStore server.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from agentlightning.store.client_server import LightningStoreServer
|
||||
from agentlightning.store.memory import InMemoryLightningStore
|
||||
|
||||
|
||||
async def main():
|
||||
store = InMemoryLightningStore()
|
||||
server = LightningStoreServer(store, host="0.0.0.0", port=23333)
|
||||
await server.start()
|
||||
|
||||
with open("docs/assets/store-openapi.json", "w") as f:
|
||||
json.dump(server.app.openapi(), f) # type: ignore
|
||||
await server.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,53 @@
|
||||
model_list:
|
||||
- model_name: gpt-4o
|
||||
litellm_params:
|
||||
model: azure/gpt-4o
|
||||
api_base: os.environ/AZURE_API_BASE
|
||||
api_version: 2025-04-01-preview
|
||||
api_key: os.environ/AZURE_API_KEY
|
||||
- model_name: gpt-4o-mini
|
||||
litellm_params:
|
||||
model: azure/gpt-4o-mini
|
||||
api_base: os.environ/AZURE_API_BASE
|
||||
api_version: 2025-04-01-preview
|
||||
api_key: os.environ/AZURE_API_KEY
|
||||
- model_name: gpt-4.1
|
||||
litellm_params:
|
||||
model: azure/gpt-4.1
|
||||
api_base: os.environ/AZURE_API_BASE
|
||||
api_version: 2025-04-01-preview
|
||||
api_key: os.environ/AZURE_API_KEY
|
||||
- model_name: gpt-4.1-mini
|
||||
litellm_params:
|
||||
model: azure/gpt-4.1-mini
|
||||
api_base: os.environ/AZURE_API_BASE
|
||||
api_version: 2025-04-01-preview
|
||||
api_key: os.environ/AZURE_API_KEY
|
||||
- model_name: gpt-4.1-nano
|
||||
litellm_params:
|
||||
model: azure/gpt-4.1-nano
|
||||
api_base: os.environ/AZURE_API_BASE
|
||||
api_version: 2025-04-01-preview
|
||||
api_key: os.environ/AZURE_API_KEY
|
||||
- model_name: o4-mini
|
||||
litellm_params:
|
||||
model: azure/o4-mini
|
||||
api_base: os.environ/AZURE_API_BASE
|
||||
api_version: 2025-04-01-preview
|
||||
api_key: os.environ/AZURE_API_KEY
|
||||
- model_name: gpt-5-nano
|
||||
litellm_params:
|
||||
model: azure/gpt-5-nano
|
||||
api_base: os.environ/AZURE_API_BASE
|
||||
api_version: 2025-04-01-preview
|
||||
api_key: os.environ/AZURE_API_KEY
|
||||
- model_name: gpt-5-mini
|
||||
litellm_params:
|
||||
model: azure/gpt-5-mini
|
||||
api_base: os.environ/AZURE_API_BASE
|
||||
api_version: 2025-04-01-preview
|
||||
api_key: os.environ/AZURE_API_KEY
|
||||
|
||||
general_settings:
|
||||
# https://github.com/BerriAI/litellm/issues/15952
|
||||
disable_responses_id_security: true
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
export
|
||||
|
||||
# Configurable port (first CLI argument, or default to 12306)
|
||||
PORT="${1:-12306}"
|
||||
|
||||
# Launch LiteLLM Proxy in background
|
||||
echo "Starting LiteLLM Proxy on port ${PORT}..."
|
||||
nohup uv run litellm --config scripts/litellm_ci.yaml --port "${PORT}" &
|
||||
|
||||
# Wait for the server to be ready
|
||||
echo "Waiting for LiteLLM Proxy to start..."
|
||||
for i in {1..30}; do
|
||||
if curl -s "http://localhost:${PORT}/v1/models" > /dev/null; then
|
||||
echo "LiteLLM Proxy is up!"
|
||||
break
|
||||
fi
|
||||
echo "Waiting... (${i})"
|
||||
# Wait for 2 seconds before checking again
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Run sanity check
|
||||
echo "Running sanity check..."
|
||||
export OPENAI_BASE_URL="http://localhost:${PORT}/"
|
||||
export OPENAI_API_KEY="dummy"
|
||||
uv run scripts/litellm_sanity_check.py
|
||||
|
||||
echo "Sanity check complete!"
|
||||
@@ -0,0 +1,55 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Utility script to perform a sanity check on LiteLLM proxy server."""
|
||||
|
||||
import sys
|
||||
|
||||
import openai
|
||||
|
||||
|
||||
def main() -> None:
|
||||
client = openai.OpenAI(timeout=30.0)
|
||||
models = client.models.list()
|
||||
print("Available models:", models)
|
||||
|
||||
total_requests = 0
|
||||
success_count = 0
|
||||
|
||||
for model in models.data:
|
||||
try:
|
||||
total_requests += 1
|
||||
response = client.chat.completions.create(
|
||||
model=model.id,
|
||||
messages=[{"role": "user", "content": "Hello!"}],
|
||||
)
|
||||
print(f"Chat completion from model {model.id}:", response)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
print(f"Chat completion failed for model {model.id}: {e}")
|
||||
|
||||
try:
|
||||
total_requests += 1
|
||||
response = client.responses.create(
|
||||
model=model.id,
|
||||
input="Hello, world!",
|
||||
)
|
||||
print(f"Response from model {model.id}:", response)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
print(f"Response failed for model {model.id}: {e}")
|
||||
|
||||
if total_requests == 0:
|
||||
print("No requests made.")
|
||||
sys.exit(1)
|
||||
|
||||
success_rate = success_count / total_requests
|
||||
print(f"Success rate: {success_rate * 100:.2f}% ({success_count}/{total_requests})")
|
||||
|
||||
if success_rate >= 0.8:
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd docker
|
||||
|
||||
# Setup data directories
|
||||
./setup.sh
|
||||
|
||||
# Start Dockers
|
||||
docker compose -f compose.mongo.yml up -d
|
||||
|
||||
SERVICE_NAME=mongo
|
||||
TIMEOUT=60 # seconds
|
||||
SLEEP=2
|
||||
|
||||
cid="$(docker compose -f compose.mongo.yml ps -q "$SERVICE_NAME")"
|
||||
if [ -z "$cid" ]; then
|
||||
echo "Service $SERVICE_NAME is not running"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Waiting for $SERVICE_NAME to become healthy..."
|
||||
end=$((SECONDS + TIMEOUT))
|
||||
|
||||
while [ "$SECONDS" -lt "$end" ]; do
|
||||
status="$(docker inspect -f '{{.State.Health.Status}}' "$cid")"
|
||||
echo "Current status: $status"
|
||||
|
||||
if [ "$status" = "healthy" ]; then
|
||||
echo "$SERVICE_NAME is healthy ✅"
|
||||
exit 0
|
||||
elif [ "$status" = "unhealthy" ]; then
|
||||
echo "$SERVICE_NAME is unhealthy ❌"
|
||||
docker logs "$cid" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep "$SLEEP"
|
||||
done
|
||||
|
||||
echo "Timed out waiting for $SERVICE_NAME to become healthy after ${TIMEOUT}s"
|
||||
docker logs "$cid" || true
|
||||
exit 1
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// MongoDB replica set initialization script.
|
||||
// Use this if you are accessing MongoDB from the **host**.
|
||||
|
||||
rs.initiate({
|
||||
_id: "rs0",
|
||||
members: [{ _id: 0, host: "localhost:27017" }],
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// MongoDB replica set initialization script.
|
||||
// Use this if you are accessing MongoDB from another **container**.
|
||||
// `mongodb_init_rs_host.js` is the counterpart if accessing from the host.
|
||||
|
||||
rs.initiate({
|
||||
_id: "rs0",
|
||||
members: [{ _id: 0, host: "mongo:27017" }],
|
||||
});
|
||||
|
||||
db.setProfilingLevel(2);
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
ray stop -v --force --grace-period 60
|
||||
ps aux
|
||||
env RAY_DEBUG=legacy HYDRA_FULL_ERROR=1 VLLM_USE_V1=1 ray start --head --dashboard-host=0.0.0.0
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
set -ex
|
||||
python -m pip install --upgrade --no-cache-dir pip
|
||||
pip install --no-cache-dir -e .[dev,agent,apo]
|
||||
# Upgrade agentops to the latest version
|
||||
pip install --no-cache-dir -U agentops
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
set -ex
|
||||
|
||||
python -m pip install --upgrade --no-cache-dir pip
|
||||
|
||||
pip install --no-cache-dir packaging ninja numpy pandas ipython ipykernel gdown wheel setuptools
|
||||
# This has to be pinned for VLLM to work.
|
||||
pip install --no-cache-dir torch==2.8.0 torchvision==0.23.0 --index-url https://download.pytorch.org/whl/cu128
|
||||
pip install --no-cache-dir flash-attn --no-build-isolation
|
||||
# This must match pytorch version.
|
||||
pip install --no-cache-dir vllm==0.10.2
|
||||
# Latest VERL release version.
|
||||
# FIXME: Make VERL 0.5.0 work
|
||||
pip install --no-cache-dir "verl<0.6.0"
|
||||
|
||||
pip install --no-cache-dir -e .[dev,agent,trl,apo]
|
||||
# Upgrade agentops to the latest version
|
||||
pip install --no-cache-dir -U agentops
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
set -ex
|
||||
python -m pip install --upgrade --no-cache-dir pip
|
||||
pip install --no-cache-dir -e .[dev,agent,apo]
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
set -ex
|
||||
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
pip install --no-cache-dir packaging ninja numpy pandas ipython ipykernel gdown wheel setuptools
|
||||
pip install --no-cache-dir torch==2.7.0 torchvision==0.22.0 torchaudio==2.7.0 --index-url https://download.pytorch.org/whl/cu128
|
||||
pip install --no-cache-dir --no-deps trl unsloth # For type checking, not for running examples
|
||||
pip install --no-cache-dir transformers==4.53.3
|
||||
pip install --no-cache-dir flash-attn==2.8.1 --no-build-isolation
|
||||
pip install --no-cache-dir vllm==0.9.2
|
||||
pip install --no-cache-dir verl==0.5.0
|
||||
|
||||
pip install --no-cache-dir -e .[dev,agent,apo]
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
set -ex
|
||||
python -m pip install --upgrade --no-cache-dir pip
|
||||
|
||||
# CPU version full installation
|
||||
|
||||
pip install --no-cache-dir packaging ninja numpy pandas ipython ipykernel gdown wheel setuptools
|
||||
pip install --no-cache-dir vllm # pytorch auto installed when installing vllm
|
||||
pip install --no-cache-dir --no-deps trl unsloth
|
||||
pip install --no-cache-dir verl==0.5.0
|
||||
|
||||
pip install --no-cache-dir -e .[dev,agent,apo]
|
||||
@@ -0,0 +1,106 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
import wandb
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="Validate a Weights & Biases run for reward/trace rollouts.")
|
||||
parser.add_argument("project", help="W&B project name")
|
||||
parser.add_argument("run_name", help="W&B run display name")
|
||||
parser.add_argument(
|
||||
"--reward-tolerance",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Allowed difference between first and last val/n_rollouts_w_reward",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--trace-tolerance",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Allowed difference between first and last val/n_rollouts_w_trace",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
args = parse_args()
|
||||
|
||||
project = args.project
|
||||
run_name = args.run_name
|
||||
api = wandb.Api()
|
||||
entity_name = api.default_entity
|
||||
print("Default entity:", entity_name)
|
||||
print("Project:", project)
|
||||
print("Run name:", run_name)
|
||||
|
||||
runs = api.runs(f"{entity_name}/{project}", filters={"displayName": run_name})
|
||||
for run in runs:
|
||||
print(f"Found run: {run.name} (ID: {run.id})")
|
||||
if run.name == run_name:
|
||||
break
|
||||
else:
|
||||
print(f"::error::Run with name '{run_name}' not found in project '{project}'.")
|
||||
sys.exit(1)
|
||||
|
||||
hist = run.history(
|
||||
keys=["val/reward", "val/n_rollouts_w_reward", "val/n_rollouts_w_trace", "val/mean_response_length"], pandas=True
|
||||
)
|
||||
print("History:", hist)
|
||||
if hist.empty:
|
||||
print("::error::No history found for the run.")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# Check whether all rollouts have (approximately) succeeded
|
||||
first_row = hist.iloc[0]
|
||||
last_row = hist.iloc[-1]
|
||||
|
||||
first_reward_rollouts = first_row["val/n_rollouts_w_reward"]
|
||||
last_reward_rollouts = last_row["val/n_rollouts_w_reward"]
|
||||
reward_diff = abs(first_reward_rollouts - last_reward_rollouts)
|
||||
|
||||
if reward_diff > args.reward_tolerance or (first_reward_rollouts == 0 and last_reward_rollouts == 0):
|
||||
print(
|
||||
"::error::Some rollouts have failed to produce rewards: "
|
||||
f"{first_reward_rollouts} -> {last_reward_rollouts} "
|
||||
f"(tolerance={args.reward_tolerance})"
|
||||
)
|
||||
sys.exit(1)
|
||||
elif first_reward_rollouts != last_reward_rollouts:
|
||||
print(
|
||||
"::warning::First and last val/n_rollouts_w_reward are different: "
|
||||
f"{first_reward_rollouts} -> {last_reward_rollouts}"
|
||||
)
|
||||
|
||||
first_trace_rollouts = first_row["val/n_rollouts_w_trace"]
|
||||
last_trace_rollouts = last_row["val/n_rollouts_w_trace"]
|
||||
trace_diff = abs(first_trace_rollouts - last_trace_rollouts)
|
||||
|
||||
if trace_diff > args.trace_tolerance or (first_trace_rollouts == 0 and last_trace_rollouts == 0):
|
||||
print(
|
||||
"::error::Some rollouts have failed to produce traces: "
|
||||
f"{first_trace_rollouts} -> {last_trace_rollouts} "
|
||||
f"(tolerance={args.trace_tolerance})"
|
||||
)
|
||||
sys.exit(1)
|
||||
elif first_trace_rollouts != last_trace_rollouts:
|
||||
print(
|
||||
"::warning::First and last val/n_rollouts_w_trace are different: "
|
||||
f"{first_trace_rollouts} -> {last_trace_rollouts}"
|
||||
)
|
||||
|
||||
val_mean_response = last_row["val/mean_response_length"]
|
||||
if val_mean_response < 1:
|
||||
print(f"::error::Mean response length is too short: {val_mean_response} (expected >= 1)")
|
||||
sys.exit(1)
|
||||
|
||||
first_reward, last_reward = first_row["val/reward"], last_row["val/reward"]
|
||||
if last_reward <= first_reward:
|
||||
print(
|
||||
f"::warning title=Training no improvement::No improvement (run_name={run_name} start={first_reward:.4f}, end={last_reward:.4f})"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"::notice title=Training completed::Run has improved (run_name={run_name} start={first_reward:.4f}, end={last_reward:.4f})"
|
||||
)
|
||||
@@ -0,0 +1,249 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Usage example:
|
||||
|
||||
python scripts/wandb_download_result.py AgentLightning \
|
||||
--runs spider_agl_v0_2 \
|
||||
--metrics training/reward val/reward \
|
||||
--out docs/assets/sql-agent-training-result.json \
|
||||
--step 16
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import wandb
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Fetch metrics from Weights & Biases runs and output Chart.js-ready JSON. "
|
||||
"Aggregates by step bins to tame long x-axes."
|
||||
)
|
||||
)
|
||||
p.add_argument(
|
||||
"project",
|
||||
help="W&B project name (e.g., 'my-project'). Uses your default entity unless --entity is set.",
|
||||
)
|
||||
p.add_argument(
|
||||
"--entity",
|
||||
default=None,
|
||||
help="W&B entity (team/user). If omitted, uses wandb.Api().default_entity.",
|
||||
)
|
||||
p.add_argument(
|
||||
"--runs",
|
||||
nargs="+",
|
||||
required=True,
|
||||
help="Run names (display names) to include. Example: --runs a b c",
|
||||
)
|
||||
p.add_argument(
|
||||
"--metrics",
|
||||
nargs="+",
|
||||
required=True,
|
||||
help="Metric keys to fetch. Example: --metrics train/loss val/acc",
|
||||
)
|
||||
p.add_argument(
|
||||
"--step",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Aggregate step size in _step units (e.g., 16 groups steps into bins of 16). Default: 1 (no binning).",
|
||||
)
|
||||
p.add_argument(
|
||||
"--out",
|
||||
default="wandb_result.json",
|
||||
help="Output file name. Default: 'wandb_result.json'",
|
||||
)
|
||||
p.add_argument(
|
||||
"--label-format",
|
||||
default="{run}:{metric}",
|
||||
help="Dataset label format. You can use {run} and {metric}. Default: '{run}:{metric}'",
|
||||
)
|
||||
p.add_argument(
|
||||
"--strict",
|
||||
action="store_true",
|
||||
help="If set, exit with nonzero code when a run or metric is missing.",
|
||||
)
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def fetch_runs(api: wandb.Api, entity: str, project: str, run_names: List[str]) -> Dict[str, wandb.Run]:
|
||||
"""
|
||||
Fetch runs by displayName matching any in run_names.
|
||||
"""
|
||||
name_set = set(run_names)
|
||||
found: Dict[str, wandb.Run] = {}
|
||||
|
||||
# W&B filtering supports 'displayName'
|
||||
# We fetch all runs in the project once, then pick matching ones to be robust across filters/backends.
|
||||
# If the project is huge, you can optimize to paginate/stop early—here we walk until we’ve found all.
|
||||
for run in api.runs(f"{entity}/{project}"):
|
||||
dn = getattr(run, "name", None) or getattr(run, "displayName", None)
|
||||
# run.name is usually the short name; W&B Python public API exposes it as .name
|
||||
if dn in name_set and dn not in found:
|
||||
found[dn] = run
|
||||
if len(found) == len(name_set):
|
||||
break
|
||||
|
||||
return found
|
||||
|
||||
|
||||
def aggregate_history(df: pd.DataFrame, metrics: List[str], step: int) -> pd.DataFrame:
|
||||
"""
|
||||
Given a history dataframe with '_step' and metric columns,
|
||||
aggregate by floor(_step/step)*step and average metric values per bin.
|
||||
"""
|
||||
if "_step" not in df.columns:
|
||||
raise ValueError("History dataframe missing required '_step' column.")
|
||||
|
||||
if step < 1:
|
||||
step = 1
|
||||
|
||||
# Drop rows where all requested metrics are NaN to avoid empty bins
|
||||
keep_mask = df[metrics].notna().any(axis=1)
|
||||
df = df.loc[keep_mask].copy()
|
||||
|
||||
# Compute bin: bin is rounded to the nearest multiples of step
|
||||
df["_bin"] = np.round(df["_step"] / step) * step
|
||||
|
||||
# Group by bin and average each metric
|
||||
grouped = df.groupby("_bin", as_index=False)[metrics].mean()
|
||||
|
||||
# Ensure bins are sorted
|
||||
grouped = grouped.sort_values("_bin").reset_index(drop=True)
|
||||
return grouped
|
||||
|
||||
|
||||
def build_chartjs(
|
||||
per_run_metric_df: Dict[Tuple[str, str], pd.DataFrame],
|
||||
label_format: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Build a Chart.js line chart dataset:
|
||||
labels: union of all bins across runs (sorted)
|
||||
datasets: one per (run, metric) pair, aligned to labels, with None for missing points
|
||||
"""
|
||||
# Union of all bins
|
||||
all_bins = set()
|
||||
for df in per_run_metric_df.values():
|
||||
all_bins.update(df["_bin"].tolist())
|
||||
labels = sorted(all_bins)
|
||||
|
||||
# Chart.js wants arrays of primitive x labels (we'll use the bin starts)
|
||||
# If you want to render actual x=_step values, labels are these bin starts.
|
||||
datasets = []
|
||||
for (run_name, metric), df in per_run_metric_df.items():
|
||||
series_map = dict(zip(df["_bin"].tolist(), df[metric].tolist()))
|
||||
data = [series_map.get(b, None) for b in labels]
|
||||
datasets.append(
|
||||
{
|
||||
"label": label_format.format(run=run_name, metric=metric),
|
||||
"data": data,
|
||||
# Chart.js can infer styles; consumers can style further on the frontend
|
||||
"spanGaps": True, # nicer lines across missing bins
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"type": "line",
|
||||
"data": {
|
||||
"labels": labels,
|
||||
"datasets": datasets,
|
||||
},
|
||||
"options": {
|
||||
"interaction": {"mode": "nearest", "intersect": False},
|
||||
"plugins": {
|
||||
"legend": {"display": True, "position": "top"},
|
||||
"title": {"display": True, "text": "W&B Metrics (binned by step)"},
|
||||
},
|
||||
"scales": {
|
||||
"x": {"title": {"display": True, "text": "Step (bin start)"}},
|
||||
"y": {"title": {"display": True, "text": "Value"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
api = wandb.Api()
|
||||
entity = args.entity or api.default_entity
|
||||
if not entity:
|
||||
print("::error::Unable to determine W&B entity. Pass --entity.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
runs = fetch_runs(api, entity, args.project, args.runs)
|
||||
missing = [r for r in args.runs if r not in runs]
|
||||
if missing:
|
||||
msg = f"Runs not found: {', '.join(missing)}"
|
||||
if args.strict:
|
||||
print(f"::error::{msg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"::warning::{msg}", file=sys.stderr)
|
||||
|
||||
if not runs:
|
||||
print("::error::No matching runs found.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
per_run_metric_df: Dict[Tuple[str, str], pd.DataFrame] = {}
|
||||
|
||||
for run_name, run in runs.items():
|
||||
# Fetch each metric separately to avoid losing sparse metrics due to row intersection.
|
||||
for metric in args.metrics:
|
||||
hist = run.history(keys=["_step", metric], pandas=True)
|
||||
if hist is None or hist.empty:
|
||||
msg = f"No history for run '{run_name}' (metric '{metric}')."
|
||||
if args.strict:
|
||||
print(f"::error::{msg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"::warning::{msg}", file=sys.stderr)
|
||||
continue
|
||||
# Ensure numeric _step
|
||||
if "_step" not in hist.columns:
|
||||
print(
|
||||
f"::warning::Run '{run_name}' has no '_step' column; skipping metric '{metric}'.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
continue
|
||||
|
||||
# Clean to numeric where possible
|
||||
hist["_step"] = pd.to_numeric(hist["_step"], errors="coerce")
|
||||
hist = hist.dropna(subset=["_step"])
|
||||
hist["_step"] = hist["_step"].astype(int)
|
||||
# Aggregate per metric; dense metrics can be tamed with --step (e.g., 16)
|
||||
grouped = aggregate_history(hist, [metric], args.step)
|
||||
if metric not in grouped.columns:
|
||||
msg = f"Metric '{metric}' not found in run '{run_name}'."
|
||||
if args.strict:
|
||||
print(f"::error::{msg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"::warning::{msg}", file=sys.stderr)
|
||||
continue
|
||||
# Keep only _bin and the single metric for simpler merging later
|
||||
per_run_metric_df[(run_name, metric)] = grouped[["_bin", metric]].copy()
|
||||
|
||||
if not per_run_metric_df:
|
||||
print("::error::No data collected for any run/metric.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
chart = build_chartjs(per_run_metric_df, args.label_format)
|
||||
|
||||
payload = json.dumps(chart, ensure_ascii=False)
|
||||
if args.out:
|
||||
with open(args.out, "w", encoding="utf-8") as f:
|
||||
f.write(payload)
|
||||
print(f"Wrote Chart.js JSON to: {args.out}")
|
||||
else:
|
||||
print(payload)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user