diff --git a/skills/cloud/skill-registry/SKILL.md b/skills/cloud/skill-registry/SKILL.md new file mode 100644 index 0000000000..edeb53df1a --- /dev/null +++ b/skills/cloud/skill-registry/SKILL.md @@ -0,0 +1,78 @@ +--- +name: skill-registry +description: > + Interact with the Gemini Enterprise Agent Platform Skill Registry to create + and search for available skills. Use this skill to enable agents to register + functionality or discover new capabilities. +--- + +# Skill Registry + +This skill provides instructions for interacting with the **Skill Registry** on +the Gemini Enterprise Agent Platform. + +## Core Capabilities + +- **Skill Discovery** - Query the registry to easily search, list, get + specific skills, and inspect revision histories. +- **Skill Lifecycle Management** - Upload, update, or permanently delete + skills. +- **Operation Monitoring** - Utility to check the completion status of + long-running state changes (LROs). +- **Generate Skill** - Automate the initial scaffolding of new agent skills + locally. + +## Core Directives + +- **Mandatory Validation**: ALWAYS execute the environment validation check + before performing any operations. + + Before any operation, you **must** validate the core environment. + + ```bash + # Execute the validation script + python3 scripts/validate_env.py + ``` + +## Prerequisites & Authentication + +### Library & Authentication + +Ensure you have the latest Google Cloud credentials and libraries installed. + +```bash +# Install required libraries +pip install google-auth requests + +# Authenticate with Google Cloud +gcloud auth application-default login +``` + +### Environment Variables + +The following variables are required for operations: + +- `GCP_PROJECT_ID`: Your Google Cloud Project ID. +- `GCP_LOCATION`: The region (e.g., `us-central1`). + +-------------------------------------------------------------------------------- + +## Quickstart + +Quickly search for available skills in the registry: + +```bash +python3 scripts/skill_registry_ops.py search \ + --query "test skill" \ + --top-k 5 +``` + +-------------------------------------------------------------------------------- + +## Operations + +- **Skill Discovery**: [query-skills.md](references/query-skills.md) +- **Skill Lifecycle**: [manage-skills.md](references/manage-skills.md) +- **Monitor Operations**: + [monitor-operations.md](references/monitor-operations.md) +- **Generate Skill**: [generate-skill.md](references/generate-skill.md) diff --git a/skills/cloud/skill-registry/references/generate-skill.md b/skills/cloud/skill-registry/references/generate-skill.md new file mode 100644 index 0000000000..a49d965423 --- /dev/null +++ b/skills/cloud/skill-registry/references/generate-skill.md @@ -0,0 +1,38 @@ +# Generate Skill + +## Description + +GenerateSkill automates the *initial* scaffolding of new agent skills by generating standardized documentation (`SKILL.md`) and directory structures based on user requirements. Note: This tool serves strictly as a starting point. It generates a foundational draft and requires human intervention to refine the logic, review the architecture, and ensure the final skill meets production-level quality standards. + +**Use when:** + +* Scaffolding a new agent skill from scratch. +* Establishing a standardized directory structure for a new tool. +* Drafting a properly formatted `SKILL.md` for a specific use case. + +**Don't use when:** + +* Executing an existing skill or performing a general query. +* Writing general code outside of a skill directory. + +--- + +## Directory Structure + +When generating a new skill, the following standardized architecture should be established: + +* **`SKILL.md`**: The core documentation and instruction set for the skill *(Required)*. +* **`references/`**: Directory for storing heavy external documentation, API specs, or knowledge bases *(Optional)*. +* **`scripts/`**: Directory for executable scripts, helper functions, or setup files *(Optional)*. Offload complex code snippets, deterministic helper functions, or repetitive setup tasks into this directory to keep `SKILL.md` lean and focused entirely on high-level instructions and usage patterns. +* **`assets/`**: Directory for static files, templates, or media used by the skill *(Optional)*. + +--- + +## Execution Workflow + +To successfully generate and deliver a new skill draft, follow these sequential steps: + +1. **Requirement Gathering:** Analyze the user's prompt to understand the purpose, inputs, outputs, and constraints of the desired skill. +2. **Drafting:** Generate the `SKILL.md` content based on the gathered requirements. Ensure the description is concise (under 300 words) and explicitly defines "Use when" and "Don't use when" conditions. Identify and map out necessary optional directories (`references/`, `scripts/`, `assets/`) if applicable, ensuring that any complex or repetitive code logic is offloaded into the `scripts/` directory. +3. **Validation:** Automatically parse and validate the drafted `SKILL.md` to ensure strictly valid Markdown formatting (e.g., correct header nesting, closed tags, proper list syntax). Fix any errors before proceeding. +4. **Review Request:** Present the generated `SKILL.md` and directory structure to the user. Explicitly request their review and manual revision, reiterating that human evaluation is required to finalize the draft for production. diff --git a/skills/cloud/skill-registry/references/manage-skills.md b/skills/cloud/skill-registry/references/manage-skills.md new file mode 100644 index 0000000000..79fefdbda9 --- /dev/null +++ b/skills/cloud/skill-registry/references/manage-skills.md @@ -0,0 +1,71 @@ +# Skill Lifecycle Management + +This document covers state-changing actions (uploading, updating, deleting) +for a skill. + +## Upload Skill +Upload a new skill into the Skill Registry using either a zipped package or a +folder. + +### Supported Flags + +* `--skill-id` (Required): The unique identifier for the skill. +* `--display-name` (Required): The human-readable name of the skill. +* `--description` (Required): A description of what the skill does. +* `--zip-file` (Required, mutually exclusive with `--folder`): Path to a local + `.zip` file containing the skill. +* `--folder` (Required, mutually exclusive with `--zip-file`): Path to a local + folder containing the skill. + +```bash +# Option 1: Upload a skill from a folder (recommended) +python3 scripts/skill_registry_ops.py upload \ + --skill-id "my-sample-skill" \ + --display-name "My Sample Skill" \ + --description "A test skill uploaded via script." \ + --folder "/path/to/skill/folder" + +# Option 2: Upload a skill using a .zip file +python3 scripts/skill_registry_ops.py upload \ + --skill-id "my-sample-skill" \ + --display-name "My Sample Skill" \ + --description "A test skill uploaded via script." \ + --zip-file "/path/to/skill.zip" +``` +*Note: This returns a long-running operation ID. See `monitor-operations.md`.* + +## Update Skill +Update an existing skill's metadata or files. At least one update parameter +must be provided (`--display-name`, `--description`, `--zip-file`, or +`--folder`). + +### Supported Flags + +* `--skill-id` (Required): The unique identifier for the skill. +* `--display-name` (Optional): A new display name for the skill. +* `--description` (Optional): A new description for the skill. +* `--zip-file` (Optional, Mutually exclusive with `--folder`): Path to a new + `.zip` file payload. +* `--folder` (Optional, Mutually exclusive with `--zip-file`): Path to a new + folder payload. + +```bash +python3 scripts/skill_registry_ops.py update \ + --skill-id "my-sample-skill" \ + --display-name "Updated Name" \ + --description "Updated description." \ + --folder "/path/to/updated/skill/folder" +``` +*Note: This returns a long-running operation ID. See `monitor-operations.md`.* + +## Delete Skill +Remove a specific skill from the registry forever. + +### Supported Flags + +* `--skill-id` (Required): The unique identifier for the skill to delete. + +```bash +python3 scripts/skill_registry_ops.py delete --skill-id "my-skill" +``` +*Note: This returns a long-running operation ID. See `monitor-operations.md`.* diff --git a/skills/cloud/skill-registry/references/monitor-operations.md b/skills/cloud/skill-registry/references/monitor-operations.md new file mode 100644 index 0000000000..055acadcbc --- /dev/null +++ b/skills/cloud/skill-registry/references/monitor-operations.md @@ -0,0 +1,20 @@ +# Monitor Operations + +This document covers how to monitor the status of Long-Running Operations (LRO) +returned by lifecycle management actions (uploading, updating, or deleting a +skill). + +## Check Operation Status + +Check the status of a long-running operation using its `OPERATION_ID` (or full +resource name). + +### Supported Flags + +* `--operation-id` (Required): The unique identifier or full resource name of + the long-running operation returned from previous commands. + +```bash +python3 scripts/skill_registry_ops.py monitor \ + --operation-id "projects/my-project/locations/us-central1/operations/123456789" +``` diff --git a/skills/cloud/skill-registry/references/query-skills.md b/skills/cloud/skill-registry/references/query-skills.md new file mode 100644 index 0000000000..ac52524f3c --- /dev/null +++ b/skills/cloud/skill-registry/references/query-skills.md @@ -0,0 +1,67 @@ +# Skill Discovery + +This document covers safe, read-only operations for finding and inspecting +skills as well as their revision histories in the Skill Registry. + +## Search Skills +Find skills matching a semantic search term. + +### Supported Flags + +* `--query` (Required): The semantic query to find matching skills. +* `--top-k` (Optional): The maximum number of skills to return. Defaults to 5. + +```bash +python3 scripts/skill_registry_ops.py search \ + --query "test skill" \ + --top-k 5 +``` + +## List Skills +List all skills in the registry for the configured project and location. + +### Supported Flags + +*(None)* + +```bash +python3 scripts/skill_registry_ops.py list +``` + +## Get Skill +Retrieve details for a specific skill by its ID. + +### Supported Flags + +* `--skill-id` (Required): The unique identifier for the skill. + +```bash +python3 scripts/skill_registry_ops.py get --skill-id "my-skill" +``` + +## List Revisions +Inspect the history of changes / versions for a specific skill. (Read-only +metadata about lifecycle). + +### Supported Flags + +* `--skill-id` (Required): The unique identifier for the skill. + +```bash +python3 scripts/skill_registry_ops.py list-revision --skill-id "my-skill" +``` + +## Get Revision +Fetch details of a specific revision. + +### Supported Flags + +* `--skill-id` (Required): The unique identifier for the skill. +* `--revision-id` (Required): The specific revision ID to fetch (e.g., from + list-revision). + +```bash +python3 scripts/skill_registry_ops.py get-revision \ + --skill-id "my-skill" \ + --revision-id "test-revision-123" +``` diff --git a/skills/cloud/skill-registry/scripts/requirements.txt b/skills/cloud/skill-registry/scripts/requirements.txt new file mode 100644 index 0000000000..f2fcfa1a57 --- /dev/null +++ b/skills/cloud/skill-registry/scripts/requirements.txt @@ -0,0 +1,3 @@ +# Required Python packages for Skill Registry Python scripts. +google-auth +requests diff --git a/skills/cloud/skill-registry/scripts/skill_registry_ops.py b/skills/cloud/skill-registry/scripts/skill_registry_ops.py new file mode 100644 index 0000000000..63c094772a --- /dev/null +++ b/skills/cloud/skill-registry/scripts/skill_registry_ops.py @@ -0,0 +1,372 @@ +"""A script to perform operations on the Skill Registry.""" + +import argparse +import base64 +import io +import json +import os +import sys +import urllib.parse +import zipfile +import google.auth +from google.auth.transport.requests import Request +import requests + + +def get_access_token(): + credentials, _ = google.auth.default() + credentials.refresh(Request()) + return credentials.token + + +def get_endpoint(region): + return f"{region}-aiplatform.googleapis.com" + + +def upload(args): + """Uploads a new skill in the Skill Registry. + + Args: + args: The command line arguments. + """ + token = get_access_token() + endpoint = get_endpoint(args.location) + url = f"https://{endpoint}/v1beta1/projects/{args.project}/locations/{args.location}/skills?skillId={args.skill_id}" + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + + if args.zip_file: + with open(args.zip_file, "rb") as f: + zip_bytes = f.read() + elif args.folder: + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file: + for root, _, files in os.walk(args.folder): + for file in files: + file_path = os.path.join(root, file) + arcname = os.path.relpath(file_path, args.folder) + zip_file.write(file_path, arcname) + zip_bytes = zip_buffer.getvalue() + else: + raise ValueError("Must provide either --zip-file or --folder") + + zipped_filesystem = base64.b64encode(zip_bytes).decode("utf-8") + + payload = { + "displayName": args.display_name, + "description": args.description, + "zippedFilesystem": zipped_filesystem, + } + + print(f"Uploading skill {args.skill_id} at {endpoint}...") + response = requests.post(url, headers=headers, json=payload) + + if response.status_code >= 400: + print(f"Error: {response.status_code} - {response.text}") + sys.exit(1) + + print("Response:") + print(json.dumps(response.json(), indent=2)) + + +def search(args): + """Searches for skills in the Skill Registry.""" + token = get_access_token() + endpoint = get_endpoint(args.location) + query_encoded = urllib.parse.quote(args.query) + url = f"https://{endpoint}/v1beta1/projects/{args.project}/locations/{args.location}/skills:retrieve?query={query_encoded}&topK={args.top_k}" + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + + print(f"Searching skills at {endpoint} with query '{args.query}'...") + response = requests.get(url, headers=headers) + + if response.status_code >= 400: + print(f"Error: {response.status_code} - {response.text}") + sys.exit(1) + + print("Response:") + print(json.dumps(response.json(), indent=2)) + + +def get_skill(args): + """Gets a skill from the Skill Registry.""" + token = get_access_token() + endpoint = get_endpoint(args.location) + url = f"https://{endpoint}/v1beta1/projects/{args.project}/locations/{args.location}/skills/{args.skill_id}" + + headers = { + "Authorization": f"Bearer {token}", + } + + print(f"Getting skill {args.skill_id} at {endpoint}...") + response = requests.get(url, headers=headers) + + if response.status_code >= 400: + print(f"Error: {response.status_code} - {response.text}") + sys.exit(1) + + print("Response:") + print(json.dumps(response.json(), indent=2)) + + +def list_skills(args): + """Lists skills in the Skill Registry.""" + token = get_access_token() + endpoint = get_endpoint(args.location) + url = f"https://{endpoint}/v1beta1/projects/{args.project}/locations/{args.location}/skills" + + headers = { + "Authorization": f"Bearer {token}", + } + + print(f"Listing skills at {endpoint}...") + response = requests.get(url, headers=headers) + + if response.status_code >= 400: + print(f"Error: {response.status_code} - {response.text}") + sys.exit(1) + + print("Response:") + print(json.dumps(response.json(), indent=2)) + + +def delete_skill(args): + """Deletes a skill from the Skill Registry.""" + token = get_access_token() + endpoint = get_endpoint(args.location) + url = f"https://{endpoint}/v1beta1/projects/{args.project}/locations/{args.location}/skills/{args.skill_id}" + + headers = { + "Authorization": f"Bearer {token}", + } + + print(f"Deleting skill {args.skill_id} at {endpoint}...") + response = requests.delete(url, headers=headers) + + if response.status_code >= 400: + print(f"Error: {response.status_code} - {response.text}") + sys.exit(1) + + print("Response:") + print(json.dumps(response.json(), indent=2)) + + +def update_skill(args): + """Updates an existing skill in the Skill Registry.""" + token = get_access_token() + endpoint = get_endpoint(args.location) + + update_mask_parts = [] + payload = {} + + if args.display_name: + update_mask_parts.append("displayName") + payload["displayName"] = args.display_name + + if args.description: + update_mask_parts.append("description") + payload["description"] = args.description + + if args.zip_file or args.folder: + if args.zip_file: + with open(args.zip_file, "rb") as f: + zip_bytes = f.read() + elif args.folder: + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file: + for root, _, files in os.walk(args.folder): + for file in files: + file_path = os.path.join(root, file) + arcname = os.path.relpath(file_path, args.folder) + zip_file.write(file_path, arcname) + zip_bytes = zip_buffer.getvalue() + + zipped_filesystem = base64.b64encode(zip_bytes).decode("utf-8") + + update_mask_parts.append("zippedFilesystem") + payload["zippedFilesystem"] = zipped_filesystem + + if not update_mask_parts: + print( + "Error: must provide at least one field to update (--display-name," + " --description, --zip-file, --folder)" + ) + sys.exit(1) + + update_mask = ",".join(update_mask_parts) + url = f"https://{endpoint}/v1beta1/projects/{args.project}/locations/{args.location}/skills/{args.skill_id}?updateMask={update_mask}" + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + + print(f"Updating skill {args.skill_id} at {endpoint}...") + response = requests.patch(url, headers=headers, json=payload) + + if response.status_code >= 400: + print(f"Error: {response.status_code} - {response.text}") + sys.exit(1) + + print("Response:") + print(json.dumps(response.json(), indent=2)) + + +def list_skill_revision(args): + """Lists revisions of a skill in the Skill Registry.""" + token = get_access_token() + endpoint = get_endpoint(args.location) + url = f"https://{endpoint}/v1beta1/projects/{args.project}/locations/{args.location}/skills/{args.skill_id}/revisions" + + headers = { + "Authorization": f"Bearer {token}", + } + + print(f"Listing revisions for skill {args.skill_id} at {endpoint}...") + response = requests.get(url, headers=headers) + + if response.status_code >= 400: + print(f"Error: {response.status_code} - {response.text}") + sys.exit(1) + + print("Response:") + print(json.dumps(response.json(), indent=2)) + + +def get_skill_revision(args): + """Gets a specific revision of a skill from the Skill Registry.""" + token = get_access_token() + endpoint = get_endpoint(args.location) + url = f"https://{endpoint}/v1beta1/projects/{args.project}/locations/{args.location}/skills/{args.skill_id}/revisions/{args.revision_id}" + + headers = { + "Authorization": f"Bearer {token}", + } + + print( + f"Getting skill {args.skill_id} revision {args.revision_id} at" + f" {endpoint}..." + ) + response = requests.get(url, headers=headers) + + if response.status_code >= 400: + print(f"Error: {response.status_code} - {response.text}") + sys.exit(1) + + print("Response:") + print(json.dumps(response.json(), indent=2)) + + +def monitor(args): + """Monitors the status of a long-running operation.""" + token = get_access_token() + endpoint = get_endpoint(args.location) + op_id = args.operation_id.lstrip("/") + url = f"https://{endpoint}/v1beta1/{op_id}" + + headers = { + "Authorization": f"Bearer {token}", + } + + print(f"Monitoring operation {args.operation_id} at {endpoint}...") + response = requests.get(url, headers=headers) + + if response.status_code >= 400: + print(f"Error: {response.status_code} - {response.text}") + sys.exit(1) + + print("Response:") + print(json.dumps(response.json(), indent=2)) + + +def main(): + parser = argparse.ArgumentParser( + description="Skill Registry Operations Utility" + ) + parser.add_argument("--project", default=os.environ.get("GCP_PROJECT_ID")) + parser.add_argument("--location", default=os.environ.get("GCP_LOCATION")) + + subparsers = parser.add_subparsers(dest="action", required=True) + + upload_parser = subparsers.add_parser("upload") + upload_parser.add_argument("--skill-id", required=True) + upload_parser.add_argument("--display-name", required=True) + upload_parser.add_argument("--description", required=True) + + group = upload_parser.add_mutually_exclusive_group(required=True) + group.add_argument("--zip-file") + group.add_argument("--folder") + + search_parser = subparsers.add_parser("search") + search_parser.add_argument("--query", required=True) + search_parser.add_argument("--top-k", type=int, default=5) + + get_parser = subparsers.add_parser("get") + get_parser.add_argument("--skill-id", required=True) + + subparsers.add_parser("list") + + delete_parser = subparsers.add_parser("delete") + delete_parser.add_argument("--skill-id", required=True) + + update_parser = subparsers.add_parser("update") + update_parser.add_argument("--skill-id", required=True) + update_parser.add_argument("--display-name", required=False) + update_parser.add_argument("--description", required=False) + update_group = update_parser.add_mutually_exclusive_group(required=False) + update_group.add_argument("--zip-file") + update_group.add_argument("--folder") + + list_rev_parser = subparsers.add_parser("list-revision") + list_rev_parser.add_argument("--skill-id", required=True) + + get_rev_parser = subparsers.add_parser("get-revision") + get_rev_parser.add_argument("--skill-id", required=True) + get_rev_parser.add_argument("--revision-id", required=True) + + monitor_parser = subparsers.add_parser("monitor") + monitor_parser.add_argument("--operation-id", required=True) + + args = parser.parse_args() + + missing = [] + if not args.project: + missing.append("GCP_PROJECT_ID") + if not args.location: + missing.append("GCP_LOCATION") + + if missing: + print( + f"ERROR: Missing required environment variables: {', '.join(missing)}" + ) + sys.exit(1) + + if args.action == "upload": + upload(args) + elif args.action == "search": + search(args) + elif args.action == "get": + get_skill(args) + elif args.action == "list": + list_skills(args) + elif args.action == "delete": + delete_skill(args) + elif args.action == "update": + update_skill(args) + elif args.action == "list-revision": + list_skill_revision(args) + elif args.action == "get-revision": + get_skill_revision(args) + elif args.action == "monitor": + monitor(args) + + +if __name__ == "__main__": + main() diff --git a/skills/cloud/skill-registry/scripts/validate_env.py b/skills/cloud/skill-registry/scripts/validate_env.py new file mode 100644 index 0000000000..347f008003 --- /dev/null +++ b/skills/cloud/skill-registry/scripts/validate_env.py @@ -0,0 +1,18 @@ +"""Validates that required environment variables are set.""" + +import os +import sys + + +def validate_env(): + print("Validating core environment variables...") + required = ["GCP_PROJECT_ID", "GCP_LOCATION"] + missing = [v for v in required if not os.environ.get(v)] + if missing: + print(f"ERROR: Missing core variables: {', '.join(missing)}") + sys.exit(1) + print("SUCCESS: Core environment validated.") + + +if __name__ == "__main__": + validate_env()