Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions skills/swanlab-skill/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: swanlab-skill
metadata:
version: "0.1.0"
version: "0.1.1"
description: >
Interact with SwanLab — both writing tracking code (init/log/finish/multimedia) and querying
experiment data via CLI (`swanlab api`). Use this skill when the user wants to write training
Expand Down Expand Up @@ -95,27 +95,50 @@ CLI commands use `username/project_name` (project) or `username/project_name/run
| "console output" | Captured logs | `CLI_REFERENCE.md > run logs` |
| "what columns are tracked" | Metric definitions | `CLI_REFERENCE.md > run columns` |
| "check connectivity" / "can I reach swanlab" | Environment check | `swanlab ping` |
| "check login status" / "am I logged in" | Verify credentials | `swanlab verify` |

---

## Environment Connectivity

Before writing tracking code or running CLI queries, especially in `online` mode, use `swanlab ping` to verify that the current environment can reach the SwanLab server:
Before writing tracking code or running CLI queries, especially in `online` mode, run these two checks to confirm the environment is ready:

### 1. `swanlab ping` — Test network reachability

The fastest way to diagnose connectivity issues. Run it first when a user reports upload failures, login problems, or unknown mode fallbacks.

```bash
swanlab ping
# Reports: API host, web host, latency, and login status.
# If ping fails, check SWANLAB_API_HOST / network proxy / firewall settings.
```

This is the fastest way to diagnose connectivity issues — run it first when a user reports upload failures, login problems, or unknown mode fallbacks.
### 2. `swanlab verify` — Validate login credentials

After confirming the server is reachable, use `swanlab verify` to check that stored credentials are valid and have not expired. This reads the API key and host from the local `.netrc` file (created by `swanlab login`).

```bash
swanlab verify
# Validates stored API key against the server.
# Reports: which host (default https://swanlab.cn) and the logged-in username.
# Fails if: not logged in, API key is invalid, or key has expired.

swanlab verify --local
# Check local login status (.swanlab in current directory) instead of the global one.
```

**Recommended pre-flight sequence**:

1. `swanlab ping` → confirm the server is reachable
2. `swanlab verify` → confirm credentials are valid
3. Proceed with `swanlab api` queries or SDK code

---

## Behavioral Constraints

See `CLI_REFERENCE.md > Behavioral Constraints` for the full list. Key rules:

- **Never use `--all` unless the user explicitly asks for it.**
- **Always ask for specific column keys before running `run metrics`, `run medias`, or `run column`.**
- **Never dump large metric JSON directly into conversation.** Use `--save` to persist to file, then visualize with `scripts/plot_metrics.py --data file.json` or use `run summary` for aggregate stats.
- **Use `--all` only when the user explicitly asks for it** (e.g. "fetch all", "get everything", "complete list"). For paginated list commands, always use default pagination (`--page_num` / `--page_size`).
- **Always ask for specific column keys before running `run metrics`, `run medias`, or `run column`.** If the user doesn't know the key names, first run `run columns PATH` to discover them.
- **Always persist large metric data to file via `--save`**, then visualize with `scripts/plot_metrics.py --data file.json` or use `run summary` for aggregate stats.
19 changes: 17 additions & 2 deletions skills/swanlab-skill/references/CLI_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,17 @@ swanlab api run columns PATH [OPTIONS]
|--------|-------|---------|-------------|
| `--page_num` | `-n` | 1 | Page number (>= 1) |
| `--page_size` | `-s` | 20 | Page size. One of: 10, 12, 15, 20, 24, 27, 50, 100 |
| `--search` | | none | Fuzzy search keyword (matches column **name**, case-insensitive) |
| `--class` | | CUSTOM | Column class: `CUSTOM` or `SYSTEM` (see `SWANLAB_CONCEPTS.md > Column Classes`) |
| `--type` | | all | Data type filter (see `SWANLAB_CONCEPTS.md > Column Data Types` for all valid values) |
| `--all` | | false | Fetch all pages |
| `--save` | | off | Save output to file |

> **Notes on columns**:
> - Columns are **paginated** — use `--page_num` / `--page_size` to iterate in order to fetch all pages.
> - To query system metrics (CPU, GPU, memory, etc.), you must explicitly pass `--class SYSTEM`. The default is `CUSTOM` (user-defined metrics only).
> - **Resumed experiments have no system columns.** When an experiment is resumed via `swanlab.init(resume=...)`, system metrics (hardware monitoring) are not re-collected, so `--class SYSTEM` will return an empty list.

#### `swanlab api run column PATH --key KEY`

Get a single column by key name.
Expand Down Expand Up @@ -228,11 +234,14 @@ swanlab api run metrics PATH --keys KEYS [OPTIONS]
| `--range-end` | | none | Range end value (inclusive). Same type as `--range-start`. |
| `--range-head` | | none | Return only the first N data points (int >= 1). Mutually exclusive with `--range-tail`. |
| `--range-tail` | | none | Return only the last N data points (int >= 1). Mutually exclusive with `--range-head`. |
| `--range-last` | | none | Last N milliseconds of data (int >= 1). Mutually exclusive with `--range-start`/`--range-end`. Can combine with `--range-head`/`--range-tail`. |
| `--save` | | off | Save output to file |

**Range query constraints:**
- `--range-head` and `--range-tail` are mutually exclusive.
- `--range-last` is mutually exclusive with `--range-start`/`--range-end`.
- `--range-start` must be ≤ `--range-end`.
- `--range-head`/`--range-tail` can be combined with `--range-last` or `--range-start`/`--range-end`.
- Range query is only supported for SCALAR metrics. It downloads CSV data and applies client-side filtering.
- When using `--range-type timestamp`, each CSV row must have a timestamp column. Rows missing timestamps are skipped with a warning.

Expand All @@ -247,6 +256,12 @@ swanlab api run metrics PATH --keys loss --range-head 50

# Get data by timestamp range (Unix milliseconds)
swanlab api run metrics PATH --keys loss --range-type timestamp --range-start 1714368000000 --range-end 1714454400000

# Get data from the last 5 minutes
swanlab api run metrics PATH --keys loss --range-last 300000

# Get last 30 data points
swanlab api run metrics PATH --keys loss --range-tail 30
```

> **Tip**: For large metric data, use `--save` to write JSON to file, then plot with `scripts/plot_metrics.py --data file.json -k loss`. For quick stats, prefer `run summary` over full metrics.
Expand Down Expand Up @@ -409,8 +424,8 @@ swanlab api self-hosted summary [--save [FILENAME]] [--host HOST] [--api-key KEY

## Behavioral Constraints

- **Never use `--all` unless the user explicitly asks for it.** This flag bypasses pagination and fetches the entire dataset in one go, which puts heavy load on the database. For paginated list commands, always use default pagination (`--page_num` / `--page_size`). Only add `--all` if the user says something like "fetch all", "get everything", or "I want the complete list".
- **Always ask the user for specific column keys before running `run metrics`, `run medias`, or `run column`.** These commands accept a `--keys` or `--key` parameter. Querying without a specific key forces the server to scan and return data for all columns, which is expensive. If the user doesn't know the key names, first run `run columns PATH` to list available columns, then use the returned keys to make targeted queries. Never guess or fabricate key names — always discover them first.
- **Use `--all` only when the user explicitly asks for it.** This flag bypasses pagination and fetches the entire dataset in one go, which puts heavy load on the database. For paginated list commands, always use default pagination (`--page_num` / `--page_size`). Only add `--all` when the user says something like "fetch all", "get everything", or "I want the complete list".
- **Always ask the user for specific column keys before running `run metrics`, `run medias`, or `run column`.** These commands accept a `--keys` or `--key` parameter. Querying without a specific key forces the server to scan and return data for all columns, which is expensive. If the user doesn't know the key names, first run `run columns PATH` to list available columns, then use the returned keys for targeted queries. Always discover key names from `run columns` output before querying specific metrics.
- **Always add `--ignore-timestamp` for `run metrics` and `run logs`.** Unless the user specifically asks to keep timestamps, include this flag by default. It produces cleaner, more readable output by removing Unix timestamps from every data point.
- All output is JSON to stdout. Pipe to `jq` or similar tools for further processing.
- `--save` without a filename auto-generates `swanlab-YYYYMMDD_HHMMSS-xxxx.json` in the current directory.
Expand Down
10 changes: 7 additions & 3 deletions skills/swanlab-skill/references/SWANLAB_CONCEPTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,11 @@ Structure per data point:
| `--range-end` | none | End value, inclusive (int ≥ 0) |
| `--range-head` | none | First N data points (int ≥ 1). Mutually exclusive with `--range-tail` |
| `--range-tail` | none | Last N data points (int ≥ 1). Mutually exclusive with `--range-head` |
| `--range-last` | none | Last N milliseconds of data (int > 0). Mutually exclusive with `--range-start`/`--range-end`. Can combine with `--range-head`/`--range-tail`. |

- `--range-head` and `--range-tail` are mutually exclusive, acted as post-sampling.
- `--range-last` is mutually exclusive with `--range-start`/`--range-end`.
- `--range-head`/`--range-tail` can be combined with `--range-last` or `--range-start`/`--range-end`.
- `--range-start` must be ≤ `--range-end`.
- When `--range-type timestamp` is used, rows missing a timestamp column are skipped.
- Range query bypasses the sampling API entirely — it streams and filters the CSV export directly. Statistics (min/max/avg/median/latest) are still fetched via the sampling API and are **not** affected by the range filter.
Expand Down Expand Up @@ -235,16 +239,16 @@ Each experiment carries a `profile` object containing metadata about the run:

## Self-Hosted Instance

> **Self-hosted commands are only available for self-hosted (private) deployments.** If the resolved host contains `swanlab.cn`, these commands will fail — do not attempt them.
> **Self-hosted commands are only available for self-hosted (private) deployments.** Only proceed with these commands when the resolved host points to a self-hosted deployment.

SwanLab can be deployed as a self-hosted instance (private deployment). **Only use `swanlab api self-hosted` subcommands when the target is a self-hosted server, never on the public SwanLab cloud (`swanlab.cn`).**
SwanLab can be deployed as a self-hosted instance (private deployment). **Use `swanlab api self-hosted` subcommands exclusively on self-hosted deployments. Always verify the host points to a private server before invoking these commands.**

**Host detection rule**: Before using any self-hosted command, check where requests will be sent:
1. If the user passes `--host` explicitly → use that value.
2. Otherwise, check `SWANLAB_API_HOST` / `SWANLAB_WEB_HOST` environment variables.
3. Otherwise, check `.netrc` or SwanLab config (`~/.swanlab`).

If the resolved host contains `swanlab.cn`, self-hosted commands will fail and should not be attempted. Only proceed when the host points to a self-hosted deployment.
Self-hosted commands only succeed on self-hosted deployments. Always confirm the resolved host points to a private server before proceeding.

Self-hosted-specific features:

Expand Down
47 changes: 27 additions & 20 deletions swanlab/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from .column import Column, Columns
from .experiment import Experiment, Experiments
from .project import Project, Projects
from .selfhosted import SelfHosted
from .self_hosted import SelfHosted
from .typings.common import ApiColumnClassLiteral, ApiColumnDataTypeLiteral, ApiVisibilityLiteral, PaginatedQuery
from .user import User
from .utils import validate_api_path, validate_non_empty_string
Expand Down Expand Up @@ -154,7 +154,7 @@ def projects(
search: Optional[str] = None,
detail: Optional[bool] = True,
page: int = 1,
size: int = 20,
size: int = 100,
all: bool = False,
) -> Projects:
"""
Expand All @@ -165,7 +165,7 @@ def projects(
:param search: 搜索关键词
:param detail: 是否返回详细信息
:param page: 起始页码,默认 1
:param size: 每页数量,默认 20
:param size: 每页数量,默认 100
:param all: 是否获取全部数据,默认 False
"""
validate_api_path(path, segments=1, label="workspace")
Expand Down Expand Up @@ -225,15 +225,15 @@ def runs_get(
self,
path: str,
page: int = 1,
size: int = 20,
size: int = 100,
all: bool = False,
) -> Experiments:
"""
通过分页获取项目下的实验列表。

:param path: 项目路径,格式为 'username/project'
:param page: 起始页码,默认 1
:param size: 每页数量,默认 20
:param size: 每页数量,默认 100
:param all: 是否获取全部数据,默认 False
"""
validate_api_path(path, segments=2, label="project")
Expand All @@ -247,22 +247,25 @@ def columns(
self,
path: str,
page: int = 1,
size: int = 20,
size: int = 100,
search: Optional[str] = None,
column_class: ApiColumnClassLiteral = "CUSTOM",
column_type: Optional[ApiColumnDataTypeLiteral] = None,
all: bool = False,
) -> Columns:
"""
获取实验下的列列表(分页查询,支持搜索)。

:param path: 实验路径,格式为 'username/project/run_id'
:param page: 起始页码,默认 1
:param size: 每页数量,默认 20
:param search: 搜索关键词,搜索的是列的 name
:param column_class: 列的分类,CUSTOM 或 SYSTEM, 默认为 CUSTOM
:param column_type: 列的类型,如 FLOAT、STRING、IMAGE 等
:param all: 是否获取全部数据,默认 False
List columns under an experiment (paginated, with optional fuzzy search).

The ``search`` parameter performs **fuzzy matching** (case-insensitive ``contains``)
on the column ``name`` field.

:param path: Experiment path, format: ``'username/project/run_id'``
:param page: Page number, default 1
:param size: Page size, default 100
:param search: Fuzzy search keyword (matches column **name**, not key)
:param column_class: Column class, ``CUSTOM`` or ``SYSTEM``, default ``CUSTOM``
:param column_type: Column data type, e.g. ``FLOAT``, ``STRING``, ``IMAGE``
:param all: If True, fetch all pages, default False
"""
validate_api_path(path, segments=3, label="run")
query = PaginatedQuery(page=page, size=size, search=search, all=all)
Expand All @@ -282,12 +285,16 @@ def column(
column_type: Optional[ApiColumnDataTypeLiteral] = None,
) -> Column:
"""
获取单个列(通过搜索 key 匹配)。
Get a single column by key (fuzzy search, first match).

:param path: 实验路径,格式为 'username/project/run_id'
:param key: 列的键名, 输入不完整则模糊匹配 name 为首个 key.
:param column_class: 列的分类,CUSTOM 或 SYSTEM,默认 CUSTOM
:param column_type: 列的类型,如 FLOAT、STRING、IMAGE 等,默认为 None
Performs fuzzy search (``contains`` on ``name``) and returns the first matching
column. If multiple columns share a similar name, the first one (ordered by
``id DESC``) is returned.

:param path: Experiment path, format: ``'username/project/run_id'``
:param key: Column key to search, e.g. ``"loss"``, ``"acc"``
:param column_class: Column class, ``CUSTOM`` or ``SYSTEM``, default ``CUSTOM``
:param column_type: Column data type, e.g. ``FLOAT``, ``STRING``, ``IMAGE``
"""
validate_api_path(path, segments=3, label="run")
validate_non_empty_string(key, label="column key")
Expand Down
4 changes: 0 additions & 4 deletions swanlab/api/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
@description: 所有实体类的公共基类
"""

import random
import time
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Sequence, Tuple
Expand Down Expand Up @@ -141,8 +139,6 @@ def _paginate(
if not items:
break
yield from items
# 随机休眠控制 qps
time.sleep(random.random())
if page >= body.get("pages", 1):
break
if not query.all:
Expand Down
41 changes: 24 additions & 17 deletions swanlab/api/column.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@

class Column(BaseEntity):
"""
表示一个 SwanLab 实验列。
A single column in a SwanLab experiment.

支持双模式:构造时传入 data(列表迭代注入),或 data=None(按需懒加载)。
注意:列不支持单个获取 API,只能通过列表接口获取。
Supports two construction modes: pass ``data`` for eager initialization (e.g. from
list iteration), or ``data=None`` for lazy loading on first property access.

``_ensure_data()`` performs fuzzy search (``contains`` on ``name``) and returns the
first matching column. This is a soft lookup — if multiple columns share a similar
name, the first one (ordered by ``id DESC``) is returned.
"""

@staticmethod
Expand Down Expand Up @@ -150,11 +154,6 @@ def created_at(self) -> int:
"""列的创建时间戳。"""
return self._ensure_data().get("createdAt", 0)

@property
def error(self) -> Optional[Dict[str, Any]]:
"""列的错误信息。"""
return self._ensure_data().get("error", {})

def metric(
self,
sample: int = 1500,
Expand Down Expand Up @@ -203,19 +202,22 @@ def json(self) -> Dict[str, Any]:

class Columns(BaseEntity):
"""
实验下列集合的分页迭代器。
Paginated iterator over columns in an experiment.

The ``search`` parameter performs **fuzzy matching** (case-insensitive ``contains``)
on the column ``name`` field via the backend ``GET /experiment/:id/column`` endpoint.

用法::
Usage::

# 获取所有列
# All columns
for column in experiment.columns():
print(column.name, column.data_type)
print(column.name, column.column_type)

# 分页获取列(支持搜索)
for column in experiment.columns(page=1, size=20, search="loss"):
# Fuzzy search by name
for column in experiment.columns(search="loss"):
print(column.name)

# 获取全部列(自动翻页)
# Fetch all (auto-paginate)
for column in experiment.columns(all=True):
print(column.name)
"""
Expand Down Expand Up @@ -271,7 +273,7 @@ def _ensure_project_id(self) -> str:
return self._project_id

def __iter__(self) -> Iterator[Column]:
"""迭代分页获取列。"""
"""Iterate columns with automatic pagination."""
extra: Dict[str, Any] = {}
run_id = self._ensure_run_id()
if self._column_type:
Expand All @@ -281,6 +283,8 @@ def __iter__(self) -> Iterator[Column]:

for item in self._paginate(
f"/experiment/{run_id}/column",
# PaginatedQuery.all is client-side only: to_params() does not send it
# to the backend, so filtered all=True requests still use paged search.
self._query,
page_info=self._page_info,
extra=extra,
Expand Down Expand Up @@ -309,5 +313,8 @@ def total(self) -> int:
return self._page_info["total"]

def json(self) -> Dict[str, Any]:
self._page_info["list"] = [c.json() for c in self]
items = [c.json() for c in self]
self._page_info["list"] = items
# total/pages 来自后端第一页响应,始终为匹配 search 的真实总数;
# size 保持用户请求的分页大小,不因 all=True 而覆盖。
return self._page_info
Loading
Loading