chatbot / gradio /cli /commands /skills.py
siddharth24m's picture
Upload 3001 files
69ba728 verified
"""CLI command to install the Gradio skill for AI coding assistants.
Usage:
gradio skills add --cursor
gradio skills add --cursor --opencode
gradio skills add --cursor --global
gradio skills add --dest=~/my-skills
gradio skills add abidlabs/english-translator --cursor
"""
from __future__ import annotations
import os
import shutil
from pathlib import Path
from typing import Annotated, cast
import typer
from gradio_client import Client
from gradio_client.snippet import generate_code_snippets
from huggingface_hub import HfApi
SKILL_ID = "gradio"
_GITHUB_RAW = "https://raw.githubusercontent.com/gradio-app/gradio/main"
_SKILL_PREFIX = ".agents/skills/gradio"
_SKILL_FILES = ["SKILL.md", "examples.md"]
skills_app = typer.Typer(help="Manage Gradio skills for AI assistants.")
def _import_hf_skills():
try:
from huggingface_hub.cli.skills import ( # type: ignore[import-not-found]
CENTRAL_GLOBAL,
CENTRAL_LOCAL,
GLOBAL_TARGETS,
LOCAL_TARGETS,
)
except (ImportError, ModuleNotFoundError):
raise SystemExit(
"The 'gradio skills' command requires huggingface_hub >= 1.4.0.\n"
"Please upgrade: pip install --upgrade huggingface_hub"
) from None
return CENTRAL_GLOBAL, CENTRAL_LOCAL, GLOBAL_TARGETS, LOCAL_TARGETS
def _download(url: str) -> str:
from huggingface_hub.utils import get_session
try:
response = get_session().get(url)
response.raise_for_status()
except Exception as e:
raise SystemExit(
f"Failed to download {url}\n{e}\n\n"
"Make sure you have internet access. The skill files are fetched from "
"the Gradio GitHub repository."
) from e
return response.text
def _remove_existing(path: Path, force: bool) -> None:
if not (path.exists() or path.is_symlink()):
return
if not force:
raise SystemExit(
f"Skill already exists at {path}.\nRe-run with --force to overwrite."
)
if path.is_dir() and not path.is_symlink():
shutil.rmtree(path)
else:
path.unlink()
def _create_symlink(
agent_skills_dir: Path,
central_skill_path: Path,
force: bool,
skill_id: str = SKILL_ID,
) -> Path:
agent_skills_dir = agent_skills_dir.expanduser().resolve()
agent_skills_dir.mkdir(parents=True, exist_ok=True)
link_path = agent_skills_dir / skill_id
_remove_existing(link_path, force)
link_path.symlink_to(os.path.relpath(central_skill_path, agent_skills_dir))
return link_path
def _install_to(skills_dir: Path, force: bool) -> Path:
skills_dir = skills_dir.expanduser().resolve()
skills_dir.mkdir(parents=True, exist_ok=True)
dest = skills_dir / SKILL_ID
_remove_existing(dest, force)
dest.mkdir()
for fname in _SKILL_FILES:
content = _download(f"{_GITHUB_RAW}/{_SKILL_PREFIX}/{fname}")
(dest / fname).write_text(content, encoding="utf-8")
return dest
def _space_id_to_skill_id(space_id: str) -> str:
return space_id.replace("/", "-")
def _render_endpoint_section(
api_name: str, endpoint_info: dict, space_id: str, src_url: str
) -> str:
params = endpoint_info.get("parameters", [])
returns = endpoint_info.get("returns", [])
lines: list[str] = []
lines.append(f"### `{api_name}`\n")
if params:
lines.append("**Parameters:**\n")
for p in params:
ptype = p.get("python_type", {})
type_str = (
ptype.get("type", "Any") if isinstance(ptype, dict) else str(ptype)
)
name = p.get("parameter_name") or p.get("label", "input")
component = p.get("component", "")
default_info = ""
if p.get("parameter_has_default"):
default_info = f", default: `{p.get('parameter_default')}`"
required = (
" (required)" if not p.get("parameter_has_default", False) else ""
)
lines.append(
f"- `{name}` [{component}]: `{type_str}`{required}{default_info}"
)
lines.append("")
if returns:
lines.append("**Returns:**\n")
for r in returns:
rtype = r.get("python_type", {})
type_str = (
rtype.get("type", "Any") if isinstance(rtype, dict) else str(rtype)
)
label = r.get("label", "output")
component = r.get("component", "")
lines.append(f"- `{label}` [{component}]: `{type_str}`")
lines.append("")
snippets = generate_code_snippets(
api_name, endpoint_info, src_url, space_id=space_id
)
lines.append("**Python:**\n")
lines.append("```python")
lines.append(snippets["python"])
lines.append("```\n")
lines.append("**JavaScript:**\n")
lines.append("```javascript")
lines.append(snippets["javascript"])
lines.append("```\n")
lines.append("**cURL:**\n")
lines.append("```bash")
lines.append(snippets["bash"])
lines.append("```\n")
return "\n".join(lines)
def _get_space_description(space_id: str) -> str | None:
try:
info = HfApi().space_info(space_id)
return getattr(info, "short_description", None) or None
except Exception:
return None
def _generate_space_skill(space_id: str) -> tuple[str, str]:
try:
client = Client(space_id, download_files=False)
except Exception as e:
raise SystemExit(
f"Failed to connect to Space '{space_id}'.\n{e}\n\n"
"Make sure the Space exists, is public (or provide HF_TOKEN), and is running."
) from e
api_info = cast(dict, client.view_api(print_info=False, return_format="dict"))
src_url = client.src
skill_id = _space_id_to_skill_id(space_id)
space_description = _get_space_description(space_id)
lines: list[str] = []
lines.append("---")
lines.append(f"name: {skill_id}")
desc = (
f"description: Use the {space_id} Gradio Space via API. "
f"Provides Python, JavaScript, and cURL usage examples."
)
if space_description:
desc += f" Space description: {space_description}"
lines.append(desc)
lines.append("---\n")
lines.append(f"# {space_id}\n")
lines.append(
f"This skill describes how to use the {space_id} "
f"Gradio Space programmatically.\n"
)
named = api_info.get("named_endpoints", {})
unnamed = api_info.get("unnamed_endpoints", {})
if named:
lines.append("## API Endpoints\n")
for api_name, endpoint_info in named.items():
lines.append(
_render_endpoint_section(api_name, endpoint_info, space_id, src_url)
)
if not named and unnamed:
lines.append("## API Endpoints\n")
for fn_index, endpoint_info in unnamed.items():
lines.append(
_render_endpoint_section(
f"fn_index={fn_index}", endpoint_info, space_id, src_url
)
)
return skill_id, "\n".join(lines) + "\n"
def _install_space_skill(
skill_id: str, content: str, skills_dir: Path, force: bool
) -> Path:
skills_dir = skills_dir.expanduser().resolve()
skills_dir.mkdir(parents=True, exist_ok=True)
dest = skills_dir / skill_id
_remove_existing(dest, force)
dest.mkdir()
(dest / "SKILL.md").write_text(content, encoding="utf-8")
return dest
@skills_app.command(
"add",
)
def skills_add(
space_id: Annotated[
str | None,
typer.Argument(
help="HF Space ID (e.g. 'user/my-space'). If omitted, installs the general Gradio skill."
),
] = None,
cursor: Annotated[
bool, typer.Option("--cursor", help="Install for Cursor.")
] = False,
claude: Annotated[
bool, typer.Option("--claude", help="Install for Claude.")
] = False,
codex: Annotated[bool, typer.Option("--codex", help="Install for Codex.")] = False,
opencode: Annotated[
bool, typer.Option("--opencode", help="Install for OpenCode.")
] = False,
global_: Annotated[
bool,
typer.Option(
"--global",
"-g",
help="Install globally (user-level) instead of in the current project directory.",
),
] = False,
dest: Annotated[
Path | None,
typer.Option(
help="Install into a custom destination (path to skills directory)."
),
] = None,
force: Annotated[
bool,
typer.Option("--force", help="Overwrite existing skills in the destination."),
] = False,
) -> None:
"""Download and install a Gradio skill for an AI assistant.
When called without a space_id, installs the general Gradio skill.
When called with a space_id, generates and installs a skill for that
specific Gradio Space with Python, JS, and cURL usage examples.
"""
central_global, central_local, hf_global_targets, hf_local_targets = (
_import_hf_skills()
)
if not (cursor or claude or codex or opencode or dest):
raise typer.BadParameter(
"Pick a destination via --cursor, --claude, --codex, --opencode, or --dest."
)
global_targets = {**hf_global_targets, "cursor": Path("~/.cursor/skills")}
local_targets = {**hf_local_targets, "cursor": Path(".cursor/skills")}
targets_dict = global_targets if global_ else local_targets
if space_id is not None:
skill_id, content = _generate_space_skill(space_id)
print(f"Generated skill for Space '{space_id}'")
if dest:
if cursor or claude or codex or opencode or global_:
print("--dest cannot be combined with agent flags or --global.")
raise typer.Exit(code=1)
skill_dest = _install_space_skill(skill_id, content, dest, force)
print(f"Installed '{skill_id}' to {skill_dest}")
return
agent_targets: list[Path] = []
if cursor:
agent_targets.append(targets_dict["cursor"])
if claude:
agent_targets.append(targets_dict["claude"])
if codex:
agent_targets.append(targets_dict["codex"])
if opencode:
agent_targets.append(targets_dict["opencode"])
central_path = central_global if global_ else central_local
central_skill_path = _install_space_skill(
skill_id, content, central_path, force
)
print(f"Installed '{skill_id}' to central location: {central_skill_path}")
for agent_target in agent_targets:
link_path = _create_symlink(
agent_target, central_skill_path, force, skill_id=skill_id
)
print(f"Created symlink: {link_path}")
return
if dest:
if cursor or claude or codex or opencode or global_:
print("--dest cannot be combined with agent flags or --global.")
raise typer.Exit(code=1)
skill_dest = _install_to(dest, force)
print(f"Installed '{SKILL_ID}' to {skill_dest}")
return
agent_targets = []
if cursor:
agent_targets.append(targets_dict["cursor"])
if claude:
agent_targets.append(targets_dict["claude"])
if codex:
agent_targets.append(targets_dict["codex"])
if opencode:
agent_targets.append(targets_dict["opencode"])
central_path = central_global if global_ else central_local
central_skill_path = _install_to(central_path, force)
print(f"Installed '{SKILL_ID}' to central location: {central_skill_path}")
for agent_target in agent_targets:
link_path = _create_symlink(agent_target, central_skill_path, force)
print(f"Created symlink: {link_path}")