From 9d7620c14a4ce635d28f213a22e86bd7b0f431a3 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 4 Mar 2026 19:44:38 +0000 Subject: [PATCH 1/3] feat(examples): add mixed marketplace skills example Add example demonstrating how to combine local and remote skills from different sources: - Local skills from a project directory (greeting-helper) - Remote skills from OpenHands/extensions repository The example includes: - marketplace.json defining local skills with remote dependencies - A local SKILL.md following the AgentSkills standard - Main script showing skill loading, merging, and agent usage - README documentation Co-authored-by: openhands --- .../.plugin/marketplace.json | 28 +++ .../43_mixed_marketplace_skills/README.md | 116 +++++++++++ .../local_skills/greeting-helper/SKILL.md | 47 +++++ .../43_mixed_marketplace_skills/main.py | 194 ++++++++++++++++++ 4 files changed, 385 insertions(+) create mode 100644 examples/01_standalone_sdk/43_mixed_marketplace_skills/.plugin/marketplace.json create mode 100644 examples/01_standalone_sdk/43_mixed_marketplace_skills/README.md create mode 100644 examples/01_standalone_sdk/43_mixed_marketplace_skills/local_skills/greeting-helper/SKILL.md create mode 100644 examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py diff --git a/examples/01_standalone_sdk/43_mixed_marketplace_skills/.plugin/marketplace.json b/examples/01_standalone_sdk/43_mixed_marketplace_skills/.plugin/marketplace.json new file mode 100644 index 0000000000..4b59517244 --- /dev/null +++ b/examples/01_standalone_sdk/43_mixed_marketplace_skills/.plugin/marketplace.json @@ -0,0 +1,28 @@ +{ + "name": "mixed-skills-marketplace", + "description": "Example marketplace that combines local skills with remote skills from OpenHands/extensions", + "owner": { + "name": "OpenHands SDK Examples", + "email": "examples@openhands.dev" + }, + "metadata": { + "version": "1.0.0", + "pluginRoot": "./local_skills" + }, + "plugins": [ + { + "name": "greeting-helper", + "source": "./greeting-helper", + "description": "A local skill that helps generate creative greetings" + } + ], + "dependencies": { + "OpenHands/extensions": [ + { + "name": "github", + "source": "OpenHands/extensions/skills/github", + "description": "GitHub best practices and workflow guidance" + } + ] + } +} diff --git a/examples/01_standalone_sdk/43_mixed_marketplace_skills/README.md b/examples/01_standalone_sdk/43_mixed_marketplace_skills/README.md new file mode 100644 index 0000000000..28bc459014 --- /dev/null +++ b/examples/01_standalone_sdk/43_mixed_marketplace_skills/README.md @@ -0,0 +1,116 @@ +# Mixed Marketplace Skills Example + +This example demonstrates how to create a **marketplace that combines local and remote skills**, showing how teams can maintain their own custom skills while also leveraging the public OpenHands skills repository. + +## Overview + +The example includes: +- A **local skill** (`greeting-helper`) hosted in the `local_skills/` directory +- Access to **remote skills** from the [OpenHands/extensions](https://github.com/OpenHands/extensions) repository + +This pattern is useful for: +- Teams that want to maintain their own custom skills +- Projects that need to combine public skills with internal workflows +- Creating curated skill sets for specific use cases + +## Directory Structure + +``` +43_mixed_marketplace_skills/ +├── .plugin/ +│ └── marketplace.json # Marketplace definition (includes local + remote) +├── local_skills/ +│ └── greeting-helper/ +│ └── SKILL.md # Local skill following AgentSkills format +├── main.py # Main example script +└── README.md # This file +``` + +## Marketplace Definition + +The `.plugin/marketplace.json` file defines the marketplace: + +```json +{ + "name": "mixed-skills-marketplace", + "description": "Example marketplace combining local and remote skills", + "owner": {"name": "OpenHands SDK Examples"}, + "metadata": { + "pluginRoot": "./local_skills" + }, + "plugins": [ + {"name": "greeting-helper", "source": "./greeting-helper"} + ], + "dependencies": { + "OpenHands/extensions": [ + {"name": "github", "source": "OpenHands/extensions/skills/github"} + ] + } +} +``` + +## How It Works + +1. **Local Skills Loading**: Skills from `local_skills/` are loaded using `load_skills_from_dir()` +2. **Remote Skills Loading**: Skills from OpenHands/extensions are loaded using `load_public_skills()` +3. **Skill Merging**: Both are combined with local skills taking precedence +4. **Agent Context**: The combined skill set is provided to the agent + +## Running the Example + +### Dry Run (No LLM Required) +```bash +python main.py --dry-run +``` +This will show the skill loading without making LLM calls. + +### Full Run (Requires LLM API Key) +```bash +export LLM_API_KEY=your-api-key +python main.py +``` + +### Expected Output (Dry Run) +``` +================================================================================ +Part 1: Loading Local Skills from Directory +================================================================================ + +Loading local skills from: /path/to/local_skills + +Loaded local skills: + - greeting-helper: A local skill that helps generate creative greetings + +================================================================================ +Part 2: Loading Remote Skills from OpenHands/extensions +================================================================================ + +Loading public skills from https://github.com/OpenHands/extensions... + +Loaded 33 public skills from OpenHands/extensions: + - add-skill: Add an external skill from a GitHub repository... + - agent-memory: Persist and retrieve repository-specific knowledge... + ... + +================================================================================ +Part 3: Combining Local and Remote Skills +================================================================================ + +Total combined skills: 34 + - Local skills: 1 + - Public skills: 33 +``` + +## Creating Your Own Mixed Marketplace + +1. Create a `local_skills/` directory with your custom skills +2. Add a `.plugin/marketplace.json` to define the marketplace +3. Use `load_skills_from_dir()` for local skills +4. Use `load_public_skills()` for remote skills +5. Combine them in your `AgentContext` + +## See Also + +- [01_loading_agentskills](../../05_skills_and_plugins/01_loading_agentskills/) - Loading skills from disk +- [03_activate_skill.py](../03_activate_skill.py) - Basic skill activation +- [AgentSkills Specification](https://agentskills.io/specification) - Skill format standard diff --git a/examples/01_standalone_sdk/43_mixed_marketplace_skills/local_skills/greeting-helper/SKILL.md b/examples/01_standalone_sdk/43_mixed_marketplace_skills/local_skills/greeting-helper/SKILL.md new file mode 100644 index 0000000000..1a02334e10 --- /dev/null +++ b/examples/01_standalone_sdk/43_mixed_marketplace_skills/local_skills/greeting-helper/SKILL.md @@ -0,0 +1,47 @@ +--- +name: greeting-helper +description: A local skill that helps generate creative greetings in different languages and styles. +triggers: + - greeting + - hello + - salutation +--- + +# Greeting Helper Skill + +This is a **local skill** that demonstrates how skills can be loaded from a local directory +that is part of your project. + +## When to Use This Skill + +Use this skill when the user wants to: +- Generate creative greetings +- Say hello in different languages +- Create personalized salutations + +## Greeting Styles + +### Formal Greetings +- "Good morning/afternoon/evening" +- "Greetings" +- "Dear [Name]" + +### Casual Greetings +- "Hey there!" +- "Hi!" +- "What's up?" + +### Multilingual Greetings +- **Spanish**: "¡Hola!" +- **French**: "Bonjour!" +- **Japanese**: "こんにちは (Konnichiwa)!" +- **German**: "Guten Tag!" +- **Italian**: "Ciao!" + +## Instructions + +When helping with greetings: +1. Ask about the context (formal vs casual) +2. Ask about the target audience +3. Suggest appropriate greetings +4. Offer multilingual alternatives if appropriate diff --git a/examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py b/examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py new file mode 100644 index 0000000000..9297156c4e --- /dev/null +++ b/examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py @@ -0,0 +1,194 @@ +"""Example: Mixed Marketplace with Local and Remote Skills + +This example demonstrates how to create a marketplace that includes both: +1. Local skills hosted in your project directory +2. Remote skills from the OpenHands/extensions repository + +This pattern is useful for teams that want to: +- Maintain their own custom skills locally +- Leverage the public OpenHands skills repository +- Create a curated skill set for their specific workflows + +The marketplace is defined in .plugin/marketplace.json and includes: +- greeting-helper: A local skill in ./local_skills/greeting-helper/ +- github: A remote skill from OpenHands/extensions + +Directory Structure: + 43_mixed_marketplace_skills/ + ├── .plugin/ + │ └── marketplace.json # Marketplace definition + ├── local_skills/ + │ └── greeting-helper/ + │ └── SKILL.md # Local skill content + ├── main.py # This file + └── README.md # Documentation + +Usage: + # With LLM_API_KEY set + python main.py + + # Or run without LLM to just see the skill loading + python main.py --dry-run +""" + +import os +import sys +from pathlib import Path + +from pydantic import SecretStr + +from openhands.sdk import LLM, Agent, AgentContext, Conversation +from openhands.sdk.context.skills import ( + Skill, + load_public_skills, + load_skills_from_dir, +) +from openhands.sdk.tool import Tool +from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool + + +def main(): + # Get the directory containing this script + script_dir = Path(__file__).parent + local_skills_dir = script_dir / "local_skills" + + # ========================================================================= + # Part 1: Loading Local Skills + # ========================================================================= + print("=" * 80) + print("Part 1: Loading Local Skills from Directory") + print("=" * 80) + + print(f"\nLoading local skills from: {local_skills_dir}") + + # Load skills from the local directory + # This loads any SKILL.md files following the AgentSkills standard + repo_skills, knowledge_skills, local_skills = load_skills_from_dir(local_skills_dir) + + print(f"\nLoaded local skills:") + for name, skill in local_skills.items(): + print(f" - {name}: {skill.description or 'No description'}") + if skill.trigger: + print(f" Triggers: {skill.trigger.keywords}") + + # ========================================================================= + # Part 2: Loading Remote Skills from OpenHands/extensions + # ========================================================================= + print("\n" + "=" * 80) + print("Part 2: Loading Remote Skills from OpenHands/extensions") + print("=" * 80) + + print("\nLoading public skills from https://github.com/OpenHands/extensions...") + + # Load public skills from the OpenHands extensions repository + # This pulls from the default marketplace at OpenHands/extensions + public_skills = load_public_skills() + + print(f"\nLoaded {len(public_skills)} public skills from OpenHands/extensions:") + for skill in public_skills[:5]: # Show first 5 + desc = skill.description[:50] + "..." if skill.description else "No description" + print(f" - {skill.name}: {desc}") + if len(public_skills) > 5: + print(f" ... and {len(public_skills) - 5} more") + + # ========================================================================= + # Part 3: Combining Local and Remote Skills + # ========================================================================= + print("\n" + "=" * 80) + print("Part 3: Combining Local and Remote Skills") + print("=" * 80) + + # Combine all skills for the agent context + # Local skills take precedence over public skills with the same name + combined_skills: list[Skill] = [] + + # Add public skills first (lower precedence) + public_skill_names = {s.name for s in public_skills} + combined_skills.extend(public_skills) + + # Add local skills (higher precedence - will override if same name) + for name, skill in local_skills.items(): + if name in public_skill_names: + # Remove the public skill and add the local one + combined_skills = [s for s in combined_skills if s.name != name] + print(f" Local skill '{name}' overrides public skill") + combined_skills.append(skill) + + print(f"\nTotal combined skills: {len(combined_skills)}") + print(f" - Local skills: {len(local_skills)}") + print(f" - Public skills: {len(public_skills)}") + + # Show the combined skill set + local_names = set(local_skills.keys()) + print("\nSkills by source:") + print(f" Local: {list(local_names)}") + print(f" Remote (first 5): {[s.name for s in public_skills[:5]]}") + + # ========================================================================= + # Part 4: Using Skills with an Agent (Optional) + # ========================================================================= + print("\n" + "=" * 80) + print("Part 4: Using Skills with an Agent") + print("=" * 80) + + # Check for dry-run mode + if "--dry-run" in sys.argv: + print("\n[Dry run mode - skipping agent interaction]") + print("To run with an agent, remove --dry-run and set LLM_API_KEY") + return + + # Check for API key + api_key = os.getenv("LLM_API_KEY") + if not api_key: + print("\nSkipping agent demo (LLM_API_KEY not set)") + print("To run the full demo, set the LLM_API_KEY environment variable:") + print(" export LLM_API_KEY=your-api-key") + return + + # Configure LLM + model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") + print(f"\nUsing model: {model}") + + llm = LLM( + usage_id="mixed-skills-demo", + model=model, + api_key=SecretStr(api_key), + base_url=os.getenv("LLM_BASE_URL"), + ) + + # Create agent context with combined skills + agent_context = AgentContext( + skills=combined_skills, + # Disable automatic public skills loading since we already loaded them + load_public_skills=False, + ) + + # Create agent with tools + tools = [ + Tool(name=TerminalTool.name), + Tool(name=FileEditorTool.name), + ] + agent = Agent(llm=llm, tools=tools, agent_context=agent_context) + + # Create conversation + conversation = Conversation(agent=agent, workspace=os.getcwd()) + + # Test the local skill (triggered by "greeting" keyword) + print("\n--- Testing Local Skill (greeting-helper) ---") + print("Sending: 'Hello! Can you help me greet someone?'") + conversation.send_message("Hello! Can you help me greet someone?") + conversation.run() + + # Test the remote skill (triggered by "github" keyword) + print("\n--- Testing Remote Skill (github from OpenHands/extensions) ---") + print("Sending: 'Tell me about GitHub best practices'") + conversation.send_message("Tell me about GitHub best practices") + conversation.run() + + print(f"\nTotal cost: ${llm.metrics.accumulated_cost:.4f}") + print(f"EXAMPLE_COST: {llm.metrics.accumulated_cost:.4f}") + + +if __name__ == "__main__": + main() From 7105ad001a2e037febd302e5fb5f62589ba6e044 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 4 Mar 2026 19:52:12 +0000 Subject: [PATCH 2/3] fix: resolve pre-commit errors in mixed marketplace example - Remove unnecessary f-string prefix (ruff) - Use getattr for trigger attributes to satisfy pyright type checking Co-authored-by: openhands --- .../43_mixed_marketplace_skills/main.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py b/examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py index 9297156c4e..ea36ed5cca 100644 --- a/examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py +++ b/examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py @@ -66,11 +66,16 @@ def main(): # This loads any SKILL.md files following the AgentSkills standard repo_skills, knowledge_skills, local_skills = load_skills_from_dir(local_skills_dir) - print(f"\nLoaded local skills:") + print("\nLoaded local skills:") for name, skill in local_skills.items(): print(f" - {name}: {skill.description or 'No description'}") if skill.trigger: - print(f" Triggers: {skill.trigger.keywords}") + # KeywordTrigger has 'keywords', TaskTrigger has 'triggers' + trigger_values = getattr(skill.trigger, "keywords", None) or getattr( + skill.trigger, "triggers", None + ) + if trigger_values: + print(f" Triggers: {trigger_values}") # ========================================================================= # Part 2: Loading Remote Skills from OpenHands/extensions From f0e6a10085908397492ff393a1eb5c1ef903b014 Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 5 Mar 2026 04:29:53 +0000 Subject: [PATCH 3/3] fix: rename local_skills to skills for plugin format consistency Follow the standard plugin structure where skills are in a skills/ directory within the plugin root. Co-authored-by: openhands --- .../.plugin/marketplace.json | 2 +- .../43_mixed_marketplace_skills/README.md | 12 +++++------ .../43_mixed_marketplace_skills/main.py | 20 +++++++++---------- .../greeting-helper/SKILL.md | 0 4 files changed, 17 insertions(+), 17 deletions(-) rename examples/01_standalone_sdk/43_mixed_marketplace_skills/{local_skills => skills}/greeting-helper/SKILL.md (100%) diff --git a/examples/01_standalone_sdk/43_mixed_marketplace_skills/.plugin/marketplace.json b/examples/01_standalone_sdk/43_mixed_marketplace_skills/.plugin/marketplace.json index 4b59517244..d327c7f4fe 100644 --- a/examples/01_standalone_sdk/43_mixed_marketplace_skills/.plugin/marketplace.json +++ b/examples/01_standalone_sdk/43_mixed_marketplace_skills/.plugin/marketplace.json @@ -7,7 +7,7 @@ }, "metadata": { "version": "1.0.0", - "pluginRoot": "./local_skills" + "pluginRoot": "./skills" }, "plugins": [ { diff --git a/examples/01_standalone_sdk/43_mixed_marketplace_skills/README.md b/examples/01_standalone_sdk/43_mixed_marketplace_skills/README.md index 28bc459014..0311ab55d0 100644 --- a/examples/01_standalone_sdk/43_mixed_marketplace_skills/README.md +++ b/examples/01_standalone_sdk/43_mixed_marketplace_skills/README.md @@ -5,7 +5,7 @@ This example demonstrates how to create a **marketplace that combines local and ## Overview The example includes: -- A **local skill** (`greeting-helper`) hosted in the `local_skills/` directory +- A **local skill** (`greeting-helper`) hosted in the `skills/` directory - Access to **remote skills** from the [OpenHands/extensions](https://github.com/OpenHands/extensions) repository This pattern is useful for: @@ -19,7 +19,7 @@ This pattern is useful for: 43_mixed_marketplace_skills/ ├── .plugin/ │ └── marketplace.json # Marketplace definition (includes local + remote) -├── local_skills/ +├── skills/ │ └── greeting-helper/ │ └── SKILL.md # Local skill following AgentSkills format ├── main.py # Main example script @@ -36,7 +36,7 @@ The `.plugin/marketplace.json` file defines the marketplace: "description": "Example marketplace combining local and remote skills", "owner": {"name": "OpenHands SDK Examples"}, "metadata": { - "pluginRoot": "./local_skills" + "pluginRoot": "./skills" }, "plugins": [ {"name": "greeting-helper", "source": "./greeting-helper"} @@ -51,7 +51,7 @@ The `.plugin/marketplace.json` file defines the marketplace: ## How It Works -1. **Local Skills Loading**: Skills from `local_skills/` are loaded using `load_skills_from_dir()` +1. **Local Skills Loading**: Skills from `skills/` are loaded using `load_skills_from_dir()` 2. **Remote Skills Loading**: Skills from OpenHands/extensions are loaded using `load_public_skills()` 3. **Skill Merging**: Both are combined with local skills taking precedence 4. **Agent Context**: The combined skill set is provided to the agent @@ -76,7 +76,7 @@ python main.py Part 1: Loading Local Skills from Directory ================================================================================ -Loading local skills from: /path/to/local_skills +Loading local skills from: /path/to/skills Loaded local skills: - greeting-helper: A local skill that helps generate creative greetings @@ -103,7 +103,7 @@ Total combined skills: 34 ## Creating Your Own Mixed Marketplace -1. Create a `local_skills/` directory with your custom skills +1. Create a `skills/` directory with your custom skills 2. Add a `.plugin/marketplace.json` to define the marketplace 3. Use `load_skills_from_dir()` for local skills 4. Use `load_public_skills()` for remote skills diff --git a/examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py b/examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py index ea36ed5cca..54f21e1610 100644 --- a/examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py +++ b/examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py @@ -9,15 +9,15 @@ - Leverage the public OpenHands skills repository - Create a curated skill set for their specific workflows -The marketplace is defined in .plugin/marketplace.json and includes: -- greeting-helper: A local skill in ./local_skills/greeting-helper/ +The marketplace is defined in .plugin/plugin.json and includes: +- greeting-helper: A local skill in ./skills/greeting-helper/ - github: A remote skill from OpenHands/extensions Directory Structure: 43_mixed_marketplace_skills/ ├── .plugin/ │ └── marketplace.json # Marketplace definition - ├── local_skills/ + ├── skills/ │ └── greeting-helper/ │ └── SKILL.md # Local skill content ├── main.py # This file @@ -51,7 +51,7 @@ def main(): # Get the directory containing this script script_dir = Path(__file__).parent - local_skills_dir = script_dir / "local_skills" + skills_dir = script_dir / "skills" # ========================================================================= # Part 1: Loading Local Skills @@ -60,14 +60,14 @@ def main(): print("Part 1: Loading Local Skills from Directory") print("=" * 80) - print(f"\nLoading local skills from: {local_skills_dir}") + print(f"\nLoading local skills from: {skills_dir}") # Load skills from the local directory # This loads any SKILL.md files following the AgentSkills standard - repo_skills, knowledge_skills, local_skills = load_skills_from_dir(local_skills_dir) + repo_skills, knowledge_skills, skills = load_skills_from_dir(skills_dir) print("\nLoaded local skills:") - for name, skill in local_skills.items(): + for name, skill in skills.items(): print(f" - {name}: {skill.description or 'No description'}") if skill.trigger: # KeywordTrigger has 'keywords', TaskTrigger has 'triggers' @@ -113,7 +113,7 @@ def main(): combined_skills.extend(public_skills) # Add local skills (higher precedence - will override if same name) - for name, skill in local_skills.items(): + for name, skill in skills.items(): if name in public_skill_names: # Remove the public skill and add the local one combined_skills = [s for s in combined_skills if s.name != name] @@ -121,11 +121,11 @@ def main(): combined_skills.append(skill) print(f"\nTotal combined skills: {len(combined_skills)}") - print(f" - Local skills: {len(local_skills)}") + print(f" - Local skills: {len(skills)}") print(f" - Public skills: {len(public_skills)}") # Show the combined skill set - local_names = set(local_skills.keys()) + local_names = set(skills.keys()) print("\nSkills by source:") print(f" Local: {list(local_names)}") print(f" Remote (first 5): {[s.name for s in public_skills[:5]]}") diff --git a/examples/01_standalone_sdk/43_mixed_marketplace_skills/local_skills/greeting-helper/SKILL.md b/examples/01_standalone_sdk/43_mixed_marketplace_skills/skills/greeting-helper/SKILL.md similarity index 100% rename from examples/01_standalone_sdk/43_mixed_marketplace_skills/local_skills/greeting-helper/SKILL.md rename to examples/01_standalone_sdk/43_mixed_marketplace_skills/skills/greeting-helper/SKILL.md