-
Notifications
You must be signed in to change notification settings - Fork 217
feat(plugin): Support multiple marketplace registrations with auto-load semantics #2495
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
base: main
Are you sure you want to change the base?
Changes from all commits
e2a27c4
084e527
5a62253
e80a7f5
8fe1850
ddcc90a
5822988
33a45a0
8ac5072
976a9ab
1f6d823
bb94316
4664c63
964a9e9
4052e87
56011ba
abb8b9a
8c4510b
94ebd83
2bfa3f5
2b77e5a
270226b
4617485
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| # Multiple Marketplace Registrations | ||
|
|
||
| Register multiple marketplaces and load plugins on-demand. | ||
|
|
||
| ## Usage | ||
|
|
||
| ```bash | ||
| python main.py | ||
| ``` | ||
|
|
||
| ## Key Concepts | ||
|
|
||
| ```python | ||
| # Configure marketplaces in AgentContext | ||
| agent_context = AgentContext( | ||
| registered_marketplaces=[ | ||
| MarketplaceRegistration( | ||
| name="company", | ||
| source="github:company/plugins", | ||
| auto_load="all", # Load all plugins at conversation start | ||
| ), | ||
| MarketplaceRegistration( | ||
| name="experimental", | ||
| source="github:company/experimental", | ||
| # auto_load=None - registered but not auto-loaded | ||
| ), | ||
| ], | ||
| ) | ||
|
|
||
| # Create agent and conversation | ||
| agent = Agent(llm=llm, tools=tools, agent_context=agent_context) | ||
| conversation = Conversation(agent=agent, workspace=workspace) | ||
|
|
||
| # Load a plugin on-demand from registered marketplace | ||
| conversation.load_plugin("beta-tool@experimental") | ||
| ``` | ||
|
|
||
| ## Related | ||
|
|
||
| - [43_mixed_marketplace_skills](../../01_standalone_sdk/43_mixed_marketplace_skills/) - Example marketplace used here |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "name": "auto-marketplace", | ||
| "owner": {"name": "Demo"}, | ||
| "plugins": [{"name": "helper", "source": "./plugins/helper"}], | ||
| "skills": [] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "name": "helper", | ||
| "version": "1.0.0", | ||
| "description": "A helper plugin with utility skills" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| name: helper-skill | ||
| description: Provides helpful utilities and tips | ||
| --- | ||
| # Helper Skill | ||
| When asked for help or tips, provide clear and useful guidance. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "name": "demo-marketplace", | ||
| "owner": {"name": "Demo"}, | ||
| "plugins": [{"name": "greeter", "source": "./plugins/greeter"}], | ||
| "skills": [] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "name": "greeter", | ||
| "version": "1.0.0", | ||
| "description": "A greeting plugin" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| name: greeter-skill | ||
| description: Generates friendly greetings | ||
| --- | ||
| # Greeter Skill | ||
| When asked to greet someone, respond with a warm, friendly greeting. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| """Example: Multiple Marketplace Registrations | ||
|
|
||
| Demonstrates two loading strategies for marketplace plugins: | ||
|
|
||
| - auto_load="all": Plugins loaded automatically at conversation start | ||
| - auto_load=None: Plugins loaded on-demand via conversation.load_plugin() | ||
|
|
||
| This example uses pre-created marketplaces in: | ||
| - ./auto_marketplace/ - auto-loaded at conversation start | ||
| - ./demo_marketplace/ - loaded on-demand | ||
| """ | ||
|
|
||
| import os | ||
| from pathlib import Path | ||
|
|
||
| from openhands.sdk import LLM, Agent, AgentContext, Conversation | ||
| from openhands.sdk.plugin import MarketplaceRegistration | ||
|
|
||
|
|
||
| SCRIPT_DIR = Path(__file__).parent | ||
|
|
||
|
|
||
| def main(): | ||
| llm = LLM( | ||
| model=os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"), | ||
| api_key=os.getenv("LLM_API_KEY"), | ||
| base_url=os.getenv("LLM_BASE_URL"), | ||
| ) | ||
|
|
||
| # Register two marketplaces with different loading strategies | ||
| agent_context = AgentContext( | ||
| registered_marketplaces=[ | ||
| # Auto-loaded: plugins available immediately when conversation starts | ||
| MarketplaceRegistration( | ||
| name="auto", | ||
| source=str(SCRIPT_DIR / "auto_marketplace"), | ||
| auto_load="all", | ||
| ), | ||
| # On-demand: registered but not loaded until explicitly requested | ||
| MarketplaceRegistration( | ||
| name="demo", | ||
| source=str(SCRIPT_DIR / "demo_marketplace"), | ||
| # auto_load=None (default) - use load_plugin() to load | ||
| ), | ||
| ], | ||
| ) | ||
|
|
||
| agent = Agent(llm=llm, tools=[], agent_context=agent_context) | ||
| conversation = Conversation(agent=agent, workspace=os.getcwd()) | ||
|
|
||
| # The "auto" marketplace plugins are already loaded | ||
| # Now load an additional plugin on-demand from "demo" marketplace | ||
| # Format: "plugin-name@marketplace-name" (same as Claude Code plugin syntax) | ||
| conversation.load_plugin("greeter@demo") | ||
|
|
||
| resolved = conversation.resolved_plugins | ||
| if resolved: | ||
| print(f"Loaded {len(resolved)} plugin(s):") | ||
| for plugin in resolved: | ||
| print(f" - {plugin.source}") | ||
|
|
||
| # Use skills from both plugins | ||
| conversation.send_message("Give me a tip, then greet me!") | ||
| conversation.run() | ||
|
|
||
| print(f"\nEXAMPLE_COST: {llm.metrics.accumulated_cost:.4f}") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| if not os.getenv("LLM_API_KEY"): | ||
| print("Set LLM_API_KEY to run this example") | ||
| print("EXAMPLE_COST: 0") | ||
| else: | ||
| main() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| Business logic is delegated to skills_service.py. | ||
| """ | ||
|
|
||
| import warnings | ||
| from typing import Literal | ||
|
|
||
| from fastapi import APIRouter | ||
|
|
@@ -15,6 +16,7 @@ | |
| sync_public_skills, | ||
| ) | ||
| from openhands.sdk.context.skills.skill import DEFAULT_MARKETPLACE_PATH | ||
| from openhands.sdk.plugin.types import MarketplaceRegistration | ||
|
|
||
|
|
||
| skills_router = APIRouter(prefix="/skills", tags=["Skills"]) | ||
|
|
@@ -66,11 +68,22 @@ class SkillsRequest(BaseModel): | |
| load_org: bool = Field(default=True, description="Load organization-level skills") | ||
| marketplace_path: str | None = Field( | ||
| default=DEFAULT_MARKETPLACE_PATH, | ||
| deprecated=True, | ||
| description=( | ||
| "DEPRECATED: Use registered_marketplaces instead. " | ||
| "Relative marketplace JSON path for public skills. " | ||
| "Set to null to load all public skills." | ||
| ), | ||
| ) | ||
| registered_marketplaces: list[MarketplaceRegistration] = Field( | ||
| default_factory=list, | ||
| description=( | ||
| "List of marketplace registrations for skill loading. " | ||
| "Each registration specifies a marketplace source and whether to " | ||
| "auto-load its skills. When provided, this takes precedence over " | ||
| "marketplace_path for public skill filtering." | ||
| ), | ||
| ) | ||
| project_dir: str | None = Field( | ||
| default=None, description="Workspace directory path for project skills" | ||
| ) | ||
|
|
@@ -143,6 +156,26 @@ def get_skills(request: SkillsRequest) -> SkillsResponse: | |
| org_repo_url = request.org_config.org_repo_url | ||
| org_name = request.org_config.org_name | ||
|
|
||
| # Handle deprecation: prefer registered_marketplaces over marketplace_path | ||
| marketplace_path = request.marketplace_path | ||
| if request.registered_marketplaces: | ||
| # New behavior: use registered_marketplaces | ||
| # For now, we extract marketplace_path from the first 'public' registration | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is still only a partial replacement for |
||
| # with auto_load='all' for backward compatibility with the existing service | ||
| for reg in request.registered_marketplaces: | ||
| if reg.name == "public" and reg.auto_load == "all": | ||
| # Use repo_path as marketplace_path if set | ||
| marketplace_path = reg.repo_path | ||
| break | ||
| elif request.marketplace_path is not None: | ||
| # Emit deprecation warning when using old field | ||
| warnings.warn( | ||
| "SkillsRequest.marketplace_path is deprecated. " | ||
| "Use registered_marketplaces instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2, | ||
| ) | ||
|
|
||
| # Call the service | ||
| result = load_all_skills( | ||
| load_public=request.load_public, | ||
|
|
@@ -153,7 +186,8 @@ def get_skills(request: SkillsRequest) -> SkillsResponse: | |
| org_repo_url=org_repo_url, | ||
| org_name=org_name, | ||
| sandbox_exposed_urls=sandbox_urls, | ||
| marketplace_path=request.marketplace_path, | ||
| marketplace_path=marketplace_path, | ||
| registered_marketplaces=request.registered_marketplaces, | ||
| ) | ||
|
|
||
| # Convert Skill objects to SkillInfo for response | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.