-
Notifications
You must be signed in to change notification settings - Fork 16
docs: add mixed marketplace skills guide #375
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
neubig
wants to merge
8
commits into
main
Choose a base branch
from
docs/mixed-marketplace-skills-example
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+320
−0
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
b3316c7
docs: add mixed marketplace skills guide
openhands-agent 32e23db
docs: clarify marketplace skill filtering
openhands-agent a5a4609
fix: update docs to reference skills/ directory
openhands-agent 6ecfe0c
docs: clarify marketplace schema is filter list for public skills
openhands-agent 8607df1
Clarify mixed marketplace guide flow
openhands-agent 40b1924
Apply suggestion from @enyst
enyst 1aa85ed
Clarify mixed marketplace filtering docs
openhands-agent 1ee91a9
docs: clarify custom marketplace loading
openhands-agent File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,320 @@ | ||
| --- | ||
| title: Mixed Marketplace Skills | ||
| description: Combine local skills with remote skills from OpenHands extensions to create custom skill configurations. | ||
| --- | ||
|
|
||
| import RunExampleCode from "/sdk/shared-snippets/how-to-run-example.mdx"; | ||
|
|
||
| This guide shows how to combine locally-defined skills with remote skills from the [OpenHands extensions repository](https://github.com/OpenHands/extensions). | ||
|
|
||
| ## Use Case | ||
|
|
||
| Teams often need to: | ||
| - Maintain custom workflow-specific skills locally | ||
| - Use skills from OpenHands extensions | ||
| - Create curated skill sets for specific projects | ||
|
|
||
| ## Loading Pattern | ||
|
|
||
| This guide focuses on the two loader APIs used in the example: | ||
|
|
||
| | Method | Source | Use Case | | ||
| |--------|--------|----------| | ||
| | `load_skills_from_dir()` | Local directory | Project-specific skills | | ||
| | `load_public_skills()` | OpenHands/extensions or a custom skills repo | Public skills from the SDK default marketplace, or from a custom marketplace when you pass `repo_url` and `marketplace_path` | | ||
|
|
||
neubig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ## Example repository layout | ||
|
|
||
| The example repository separates local skills from the marketplace configuration that describes which remote skills should be included: | ||
|
|
||
| ```text | ||
| 43_mixed_marketplace_skills/ | ||
| ├── .plugin/ | ||
| │ └── marketplace.json | ||
| ├── local_skills/ | ||
| │ └── greeting-helper/ | ||
| │ └── SKILL.md | ||
| ├── main.py | ||
| └── README.md | ||
neubig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ``` | ||
|
|
||
| ## Marketplace format note | ||
|
|
||
| The `.plugin/marketplace.json` file follows the Claude Code plugin marketplace schema, with an OpenHands extension for direct `skills[]` entries. A minimal mixed example looks like this: | ||
|
|
||
| ```json | ||
| { | ||
| "name": "mixed-skills-marketplace", | ||
| "plugins": [], | ||
| "skills": [ | ||
| { | ||
| "name": "greeting-helper", | ||
| "source": "./local_skills/greeting-helper", | ||
| "description": "A local greeting skill" | ||
| }, | ||
| { | ||
| "name": "github", | ||
| "source": "https://github.com/OpenHands/extensions/blob/main/skills/github", | ||
| "description": "GitHub best practices from OpenHands/extensions" | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| That file is a repository-owned example of the mixed marketplace shape. | ||
|
|
||
| If you want to load a curated remote marketplace programmatically, call `load_public_skills()` with both the repository URL and the marketplace file path inside that repository: | ||
|
|
||
| ```python icon="python" | ||
| from openhands.sdk.context.skills import load_public_skills | ||
|
|
||
| public_skills = load_public_skills( | ||
| repo_url="https://github.com/my-org/my-skills", | ||
| branch="main", | ||
| marketplace_path="marketplaces/custom.json", | ||
| ) | ||
| ``` | ||
|
|
||
| `repo_url` tells the SDK which skills repository to clone, and `marketplace_path` tells it which marketplace JSON inside that repository to read. Calling `load_public_skills()` with no arguments, as the example below does, uses the SDK's default `OpenHands/extensions` marketplace instead. | ||
|
|
||
neubig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| The code example below focuses on the two underlying loader APIs (`load_skills_from_dir()` and `load_public_skills()`) so you can see exactly what each source contributes. In other words: | ||
|
|
||
neubig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - `load_skills_from_dir(local_skills_dir)` reads local `SKILL.md` files from your repository. | ||
| - `load_public_skills()` reads from the SDK's default `OpenHands/extensions` marketplace when called with no arguments. | ||
| - `load_public_skills(repo_url=..., marketplace_path=...)` reads a curated marketplace from a custom skills repository. | ||
| - `.plugin/marketplace.json` shows the equivalent repository-owned configuration, but the example script below intentionally uses the loaders directly. | ||
|
|
||
| ## Example: Combining Local and Remote Skills | ||
|
|
||
| <Note> | ||
| Full example: [examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py) | ||
| </Note> | ||
|
|
||
| ```python icon="python" expandable examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py | ||
| """Example: Mixed Marketplace Skills - Combining Local and Remote Skills | ||
|
|
||
| This example demonstrates how to create a marketplace that combines: | ||
| 1. Local skills hosted in your project directory | ||
| 2. Remote skills from the OpenHands/extensions repository | ||
|
|
||
| Use Case: | ||
| --------- | ||
| Teams often need to maintain their own custom skills while also leveraging | ||
| the community skills from OpenHands. This pattern allows you to: | ||
| - Keep specialized/private skills in your repository | ||
| - Reference public skills from OpenHands/extensions | ||
| - Create a curated skill set tailored for your workflow | ||
|
|
||
| Directory Structure: | ||
| ------------------- | ||
| 43_mixed_marketplace_skills/ | ||
| ├── .plugin/ | ||
| │ └── marketplace.json # Marketplace configuration | ||
| ├── local_skills/ | ||
| │ └── greeting-helper/ | ||
| │ └── SKILL.md # Local skill following AgentSkills standard | ||
| ├── main.py # This example script | ||
| └── README.md | ||
|
|
||
| The `.plugin/marketplace.json` file shown earlier is where you would encode the | ||
| mixed marketplace itself. In this example script we intentionally call the loader | ||
| APIs directly so you can see each source in isolation before combining them. | ||
| """ | ||
|
|
||
| import argparse | ||
| import os | ||
| from pathlib import Path | ||
|
|
||
| from pydantic import SecretStr | ||
|
|
||
| from openhands.sdk import LLM, Agent, AgentContext, Conversation | ||
| from openhands.sdk.context.skills import ( | ||
| 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(): | ||
| parser = argparse.ArgumentParser(description="Mixed Marketplace Skills Example") | ||
| parser.add_argument( | ||
| "--dry-run", | ||
| action="store_true", | ||
| help="Run without LLM (just show skill loading)", | ||
| ) | ||
| args = parser.parse_args() | ||
|
|
||
| # ========================================================================= | ||
| # Part 1: Loading Local Skills from Directory | ||
| # ========================================================================= | ||
| print("=" * 80) | ||
| print("Part 1: Loading Local Skills from Directory") | ||
| print("=" * 80) | ||
|
|
||
| script_dir = Path(__file__).parent | ||
| local_skills_dir = script_dir / "local_skills" | ||
|
|
||
| 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("\nLoaded local skills:") | ||
| for name, skill in local_skills.items(): | ||
| print(f" - {name}: {skill.description or 'No description'}") | ||
| if skill.trigger: | ||
| # 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 | ||
| # ========================================================================= | ||
| 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 call uses the SDK's default OpenHands/extensions marketplace, | ||
| # not the `.plugin/marketplace.json` file shown above. | ||
| public_skills = load_public_skills() | ||
neubig marked this conversation as resolved.
Show resolved
Hide resolved
neubig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| print(f"\nLoaded {len(public_skills)} public skills from OpenHands/extensions:") | ||
| for skill in public_skills[:5]: # Show first 5 | ||
| desc = (skill.description or "No description")[:50] | ||
| 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 skills with local skills taking precedence. | ||
| combined_skills = list(local_skills.values()) + public_skills | ||
|
|
||
| # Use dict insertion order to keep the first occurrence of each skill name, | ||
| # which means local skills win over public skills with the same name. | ||
| unique_skills_by_name = {} | ||
| for skill in combined_skills: | ||
| unique_skills_by_name.setdefault(skill.name, skill) | ||
| unique_skills = list(unique_skills_by_name.values()) | ||
|
|
||
| print(f"\nTotal combined skills: {len(unique_skills)}") | ||
| print(f" - Local skills: {len(local_skills)}") | ||
| print(f" - Public skills: {len(public_skills)}") | ||
|
|
||
| local_names = list(local_skills.keys()) | ||
| public_names = [s.name for s in public_skills[:5]] | ||
| print(f"\nSkills by source:") | ||
| print(f" Local: {local_names}") | ||
| print(f" Remote (first 5): {public_names}") | ||
|
|
||
| # ========================================================================= | ||
| # Part 4: Using Skills with an Agent | ||
| # ========================================================================= | ||
| print("\n" + "=" * 80) | ||
| print("Part 4: Using Skills with an Agent") | ||
| print("=" * 80) | ||
|
|
||
| api_key = os.getenv("LLM_API_KEY") | ||
| model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") | ||
|
|
||
| if args.dry_run or 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 | ||
|
|
||
| print(f"\nUsing model: {model}") | ||
|
|
||
| llm = LLM( | ||
| usage_id="mixed-skills-demo", | ||
| model=model, | ||
| api_key=SecretStr(api_key), | ||
| ) | ||
|
|
||
| tools = [ | ||
| Tool(name=TerminalTool.name), | ||
| Tool(name=FileEditorTool.name), | ||
| ] | ||
|
|
||
| # Create agent context with combined skills | ||
| agent_context = AgentContext(skills=unique_skills) | ||
|
|
||
| agent = Agent(llm=llm, tools=tools, agent_context=agent_context) | ||
| conversation = Conversation(agent=agent, workspace=str(script_dir)) | ||
|
|
||
| # Test the agent with a query that should trigger both local and public skills | ||
| print("\nSending message to trigger skills...") | ||
| conversation.send_message( | ||
| "Tell me about GitHub best practices. " | ||
| "Also, can you give me a creative greeting?" | ||
| ) | ||
| conversation.run() | ||
|
|
||
| print(f"\nTotal cost: ${llm.metrics.accumulated_cost:.4f}") | ||
| print(f"EXAMPLE_COST: {llm.metrics.accumulated_cost:.4f}") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
| ``` | ||
|
|
||
| <RunExampleCode path_to_script="examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py"/> | ||
|
|
||
| ## Creating a Local Skill | ||
|
|
||
| Local skills follow the [AgentSkills standard](https://agentskills.io/specification). Create a `SKILL.md` file: | ||
|
|
||
| ```markdown icon="markdown" | ||
| --- | ||
| name: greeting-helper | ||
neubig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| description: A local skill that helps generate creative greetings. | ||
| triggers: | ||
| - greeting | ||
| - hello | ||
| - salutation | ||
| --- | ||
|
|
||
| # Greeting Helper Skill | ||
|
|
||
| When asked for a greeting, provide creative options in different styles: | ||
|
|
||
| 1. **Formal**: "Good day, esteemed colleague" | ||
| 2. **Casual**: "Hey there!" | ||
| 3. **Enthusiastic**: "Hello, wonderful human!" | ||
| ``` | ||
|
|
||
| ## Skill Precedence | ||
|
|
||
| When combining skills, local skills take precedence over public skills with the same name: | ||
|
|
||
| ```python icon="python" | ||
| # Local skills override public skills with matching names | ||
| combined_skills = list(local_skills.values()) + public_skills | ||
|
|
||
| seen_names = set() | ||
| unique_skills = [] | ||
| for skill in combined_skills: | ||
| if skill.name not in seen_names: | ||
| seen_names.add(skill.name) | ||
| unique_skills.append(skill) | ||
| ``` | ||
|
|
||
| ## Next Steps | ||
|
|
||
| - **[Skills Overview](/overview/skills)** - Learn more about skill types | ||
| - **[Public Skills](/sdk/guides/skill#loading-public-skills)** - Load from OpenHands extensions | ||
| - **[Custom Tools](/sdk/guides/custom-tools)** - Create specialized tools | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.