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..d327c7f4fe --- /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": "./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..0311ab55d0 --- /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 `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) +├── 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": "./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 `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/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 `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/main.py b/examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py new file mode 100644 index 0000000000..54f21e1610 --- /dev/null +++ b/examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py @@ -0,0 +1,199 @@ +"""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/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 + ├── 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 + skills_dir = script_dir / "skills" + + # ========================================================================= + # Part 1: Loading Local Skills + # ========================================================================= + print("=" * 80) + print("Part 1: Loading Local Skills from Directory") + print("=" * 80) + + 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, skills = load_skills_from_dir(skills_dir) + + print("\nLoaded local skills:") + for name, skill in 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 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 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(skills)}") + print(f" - Public skills: {len(public_skills)}") + + # Show the combined skill set + 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]]}") + + # ========================================================================= + # 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() diff --git a/examples/01_standalone_sdk/43_mixed_marketplace_skills/skills/greeting-helper/SKILL.md b/examples/01_standalone_sdk/43_mixed_marketplace_skills/skills/greeting-helper/SKILL.md new file mode 100644 index 0000000000..1a02334e10 --- /dev/null +++ b/examples/01_standalone_sdk/43_mixed_marketplace_skills/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