diff --git a/CHANGELOG.md b/CHANGELOG.md index b2d76b7..1ea2fab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ Contributors add user-facing entries under `[Unreleased]` in the same PR. Mainta - **Citation:** Zenodo concept DOI `10.5281/zenodo.21552745` in `CITATION.cff`, README badge/Citing section, and `pyproject.toml` project URL (#269). + +### Changed + +- **`creative/bg_remover`**: Hardened the skill by reusing rembg sessions across executions, validating Base64 input and image files, rejecting empty, oversized (>25 MB), or invalid images, and automatically creating parent directories for `output_path`. Updated documentation and expanded bundle tests for the new validation behavior. + ## [0.4.7] - 2026-07-25 ### Added diff --git a/docs/skills/bg_remover.md b/docs/skills/bg_remover.md index 36823a3..3aed6a4 100644 --- a/docs/skills/bg_remover.md +++ b/docs/skills/bg_remover.md @@ -74,7 +74,7 @@ Agent routing details live in `skills/creative/bg_remover/instructions.md`. | `height` | Output image height in pixels | | `model_used` | rembg model used for processing | | `error` | Human-readable error message when `success` is `false` | -| `error_code` | `INVALID_INPUT`, `MISSING_DEPENDENCY`, or `PROCESSING_FAILED` | +| `error_code` | `INVALID_INPUT`, `FILE_NOT_FOUND`, `MISSING_DEPENDENCY`, or `PROCESSING_FAILED` | ## Input scenarios @@ -123,7 +123,7 @@ Skill instructions: when to invoke, input/output conventions, URL and cloud pre- ### The Body (`skill.py`) -Lazy-imports `rembg` and Pillow, runs `new_session` + `remove`, and returns structured JSON with transparent PNG bytes. +Lazy-imports `rembg` and Pillow, reuses cached rembg sessions, runs `remove`, and returns structured JSON with transparent PNG bytes. ## Usage Examples @@ -133,7 +133,7 @@ Use `bundle["class"]()` in the snippets below; explicit `bundle["module"].ClassN ### Runnable examples -See [examples/README.md](../../examples/README.md) for the current runnable-script inventory. There is no dedicated runnable example for this skill yet; the Claude, OpenAI, and DeepSeek sections below are **catalog snippets only** (same pattern as [PDF Form Filler](pdf_form_filler.md)). +See [examples/README.md](../../examples/README.md) for the current runnable-script inventory. See `examples/bg_remover_demo.py` for a runnable local example. The Claude, OpenAI, and DeepSeek sections below remain catalog snippets showing provider integration patterns. Sample user request: diff --git a/docs/usage/agent_loops.md b/docs/usage/agent_loops.md index 8f699ca..2384765 100644 --- a/docs/usage/agent_loops.md +++ b/docs/usage/agent_loops.md @@ -98,7 +98,7 @@ skills in one harness. | `office/pdf_form_filler` | - | `gemini_pdf_form_filler.py` | `claude_pdf_form_filler.py` | (catalog page) | (catalog page) | `ollama_skills_test.py` (multi-skill) | | `compliance/mica_module` | - | `mica_rag_flow.py` | `mica_claude_flow.py` | (catalog page) | (catalog page) | `mica_ollama_flow.py` | | `compliance/pii_masker` | `pii_guardrail_flow.py` (local execute) | (catalog page) | (catalog page) | (catalog page) | (catalog page) | (catalog page) | -| `creative/bg_remover` | (catalog page) | (catalog page) | (catalog page) | (catalog page) | (catalog page) | (catalog page) | +| `creative/bg_remover` | `bg_remover_demo.py` (local execute) | (catalog page) | (catalog page) | (catalog page) | (catalog page) | (catalog page) | | `optimization/prompt_rewriter` | `prompt_compression_demo.py` (local execute) | (catalog page) | (catalog page) | (catalog page) | (catalog page) | `ollama_skills_test.py` (multi-skill) | | `data_engineering/synthetic_generator` | `build_dataset_demo.py` (local execute, Gemini backend) | (catalog page) | (catalog page) | (catalog page) | (catalog page) | (catalog page) | | `data_engineering/novelty_extractor` | `novelty_extractor_demo.py` (local execute) | `gemini_novelty_extractor.py` | (catalog page) | (catalog page) | (catalog page) | `ollama_novelty_extractor.py` | diff --git a/examples/README.md b/examples/README.md index 6bdbf38..52c39d3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,80 +1,70 @@ -# Skillware Examples Index - -> **These are usage examples, not tests.** Runnable provider demos live here; automated tests live in `skills/**/test_skill.py` (bundle) and `tests/` (framework and optional maintainer depth). See [TESTING.md](../docs/TESTING.md). - -Runnable examples in this directory show how to load Skillware skills, adapt -them for a provider, execute local skill logic, and return tool results to an -agent loop. After `pip install skillware`, run `skillware examples` or -`skillware list --examples` from any directory to browse the index in the -terminal; when no local `examples/README.md` is present, the index is fetched -from GitHub (network required); see -[CLI reference](../docs/usage/cli.md). Provider setup details live in the usage guides: - -- [API keys for skills](../docs/usage/api_keys.md) -- [Gemini](../docs/usage/gemini.md) -- [Claude](../docs/usage/claude.md) -- [OpenAI](../docs/usage/openai.md) +# Skillware Examples Index + +> **These are usage examples, not tests.** Runnable provider demos live here; automated tests live in `skills/**/test_skill.py` (bundle) and `tests/` (framework and optional maintainer depth). See [TESTING.md](../docs/TESTING.md). + +Runnable examples in this directory show how to load Skillware skills, adapt them for a provider, execute local skill logic, and return tool results to an agent loop. After `pip install skillware`, run `skillware examples` or `skillware list --examples` from any directory to browse the index in the terminal; when no local `examples/README.md` is present, the index is fetched from GitHub (network required); see [CLI reference](../docs/usage/cli.md). Provider setup details live in the usage guides: + +- [API keys for skills](../docs/usage/api_keys.md) +- [Gemini](../docs/usage/gemini.md) +- [Claude](../docs/usage/claude.md) +- [OpenAI](../docs/usage/openai.md) - [OpenAI-compatible hosts](../docs/usage/openai_compatible.md) -- [DeepSeek](../docs/usage/deepseek.md) -- [Ollama](../docs/usage/ollama.md) -- [Install extras](../docs/usage/install_extras.md) -- [Agent loops](../docs/usage/agent_loops.md) - -Install the **skill extra** for each script (see [Install extras](../docs/usage/install_extras.md)), plus an **SDK extra** when the provider column is not local execute: - -```bash -pip install "skillware[office_pdf_form_filler]" "skillware[gemini]" -``` - -For local development with every skill and SDK: - -```bash -pip install -e ".[dev,all,agents]" -``` - -## Runnable Scripts - -| Script | Skill ID | Provider | Required extra | Required env vars | Description | -| :--- | :--- | :--- | :--- | :--- | :--- | -| `mental_coach_demo.py` | `wellness/mental_coach` | Local execute | `[wellness_mental_coach]` | None | Demonstrates coaching, crisis escalation, and blocked clinical paths locally. | -| `build_dataset_demo.py` | `data_engineering/synthetic_generator` | Local execute (Gemini backend) | `[data_engineering_synthetic_generator]`, `[gemini]` | `GOOGLE_API_KEY` | Generates a JSONL synthetic dataset with the synthetic generator skill. | -| `claude_pdf_form_filler.py` | `office/pdf_form_filler` | Claude | `[office_pdf_form_filler]`, `[claude]` | `ANTHROPIC_API_KEY` | Uses Claude with the PDF form filler skill to map instructions to fields. | -| `claude_tos_evaluator.py` | `compliance/tos_evaluator` | Claude | `[compliance_tos_evaluator]`, `[claude]` | `ANTHROPIC_API_KEY` | Runs a Claude tool loop for website automation policy review. | -| `claude_issue_resolver.py` | `dev_tools/issue_resolver` | Claude | `[dev_tools_issue_resolver]`, `[claude]` | `ANTHROPIC_API_KEY`; optional `GITHUB_TOKEN` | Claude loop for GitHub issue analysis; fetches issue data after `prepare` (sample: issue #123). | -| `claude_wallet_check.py` | `finance/wallet_screening` | Claude | `[finance_wallet_screening]`, `[claude]` | `ANTHROPIC_API_KEY`, `ETHERSCAN_API_KEY` | Screens an Ethereum wallet and returns the result through a Claude tool loop. | -| `deepseek_tos_evaluator.py` | `compliance/tos_evaluator` | DeepSeek | `[compliance_tos_evaluator]`, `[openai]` | `DEEPSEEK_API_KEY` | Uses the OpenAI-compatible DeepSeek API for terms-of-service evaluation. | -| `gemini_pdf_form_filler.py` | `office/pdf_form_filler` | Gemini | `[office_pdf_form_filler]`, `[gemini]` | `GOOGLE_API_KEY`, `ANTHROPIC_API_KEY` | Uses Gemini as the agent while the PDF skill calls Anthropic for form filling. | -| `gemini_tos_evaluator.py` | `compliance/tos_evaluator` | Gemini | `[compliance_tos_evaluator]`, `[gemini]` | `GOOGLE_API_KEY` | Runs the terms-of-service evaluator with a Gemini function-calling loop. | -| `gemini_wallet_check.py` | `finance/wallet_screening` | Gemini | `[finance_wallet_screening]`, `[gemini]` | `GOOGLE_API_KEY`, `ETHERSCAN_API_KEY` | Screens an Ethereum wallet with Gemini orchestration and Etherscan data. | -| `gemini_evm_tx_handler.py` | `defi/evm_tx_handler` | Gemini | `[defi_evm_tx_handler]`, `[gemini]` | `GOOGLE_API_KEY`; for live swaps also `AGENT_WALLET_PRIVATE_KEY`, `BASE_RPC_URL` or `ETHEREUM_RPC_URL`. Set `EVM_TX_HANDLER_EXAMPLE_DEMO=1` for mocked flow without keys. | Resolve → quote → preview → execute buy flow via Gemini tool loop (or demo mode). | -| `claude_evm_tx_handler.py` | `defi/evm_tx_handler` | Claude | `[defi_evm_tx_handler]`, `[claude]` | `ANTHROPIC_API_KEY`; for live swaps also `AGENT_WALLET_PRIVATE_KEY`, RPC URLs. Demo: `EVM_TX_HANDLER_EXAMPLE_DEMO=1`. | Claude tool loop for structured DeFi intent and optional execute after confirmation. | -| `mica_claude_flow.py` | `compliance/mica_module` | Claude | `[compliance_mica_module]`, `[claude]` | `ANTHROPIC_API_KEY` | Runs a MiCA compliance agent loop through Claude. | -| `mica_ollama_flow.py` | `compliance/mica_module` | Ollama | `[compliance_mica_module]`; install `ollama` separately | None | Runs a local Ollama MiCA flow with prompt-mode tool calling. | -| `mica_rag_flow.py` | `compliance/mica_module` | Gemini | `[compliance_mica_module]`, `[gemini]` | `GOOGLE_API_KEY` | Runs the MiCA RAG flow with Gemini. | -| `ollama_skills_test.py` | `finance/wallet_screening`, `office/pdf_form_filler`, `optimization/prompt_rewriter` | Ollama | `[finance_wallet_screening]`, `[office_pdf_form_filler]`, `[optimization_prompt_rewriter]`; install `ollama` separately | `ETHERSCAN_API_KEY`, `ANTHROPIC_API_KEY` | Loads multiple skills and tests prompt-mode tool calling with Ollama. | -| `ollama_tos_evaluator.py` | `compliance/tos_evaluator` | Ollama | `[compliance_tos_evaluator]`; install `ollama` separately | None | Runs the terms-of-service evaluator with local Ollama prompt-mode calls. | -| `openai_tos_evaluator.py` | `compliance/tos_evaluator` | OpenAI | `[compliance_tos_evaluator]`, `[openai]` | `OPENAI_API_KEY` | Runs the terms-of-service evaluator with OpenAI function calling. | +- [DeepSeek](../docs/usage/deepseek.md) +- [Ollama](../docs/usage/ollama.md) +- [Install extras](../docs/usage/install_extras.md) +- [Agent loops](../docs/usage/agent_loops.md) + +Install the **skill extra** for each script (see [Install extras](../docs/usage/install_extras.md)), plus an **SDK extra** when the provider column is not local execute: + +```bash +pip install "skillware[office_pdf_form_filler]" "skillware[gemini]" +``` + +For local development with every skill and SDK: + +```bash +pip install -e ".[dev,all,agents]" +``` + +## Runnable Scripts + +| Script | Skill ID | Provider | Required extra | Required env vars | Description | +| :--- | :--- | :--- | :--- | :--- | :--- | +| `mental_coach_demo.py` | `wellness/mental_coach` | Local execute | `[wellness_mental_coach]` | None | Demonstrates coaching, crisis escalation, and blocked clinical paths locally. | +| `build_dataset_demo.py` | `data_engineering/synthetic_generator` | Local execute (Gemini backend) | `[data_engineering_synthetic_generator]`, `[gemini]` | `GOOGLE_API_KEY` | Generates a JSONL synthetic dataset with the synthetic generator skill. | +| `claude_pdf_form_filler.py` | `office/pdf_form_filler` | Claude | `[office_pdf_form_filler]`, `[claude]` | `ANTHROPIC_API_KEY` | Uses Claude with the PDF form filler skill to map instructions to fields. | +| `claude_tos_evaluator.py` | `compliance/tos_evaluator` | Claude | `[compliance_tos_evaluator]`, `[claude]` | `ANTHROPIC_API_KEY` | Runs a Claude tool loop for website automation policy review. | +| `claude_issue_resolver.py` | `dev_tools/issue_resolver` | Claude | `[dev_tools_issue_resolver]`, `[claude]` | `ANTHROPIC_API_KEY`; optional `GITHUB_TOKEN` | Claude loop for GitHub issue analysis; fetches issue data after `prepare` (sample: issue #123). | +| `claude_wallet_check.py` | `finance/wallet_screening` | Claude | `[finance_wallet_screening]`, `[claude]` | `ANTHROPIC_API_KEY`, `ETHERSCAN_API_KEY` | Screens an Ethereum wallet and returns the result through a Claude tool loop. | +| `deepseek_tos_evaluator.py` | `compliance/tos_evaluator` | DeepSeek | `[compliance_tos_evaluator]`, `[openai]` | `DEEPSEEK_API_KEY` | Uses the OpenAI-compatible DeepSeek API for terms-of-service evaluation. | +| `gemini_pdf_form_filler.py` | `office/pdf_form_filler` | Gemini | `[office_pdf_form_filler]`, `[gemini]` | `GOOGLE_API_KEY`, `ANTHROPIC_API_KEY` | Uses Gemini as the agent while the PDF skill calls Anthropic for form filling. | +| `gemini_tos_evaluator.py` | `compliance/tos_evaluator` | Gemini | `[compliance_tos_evaluator]`, `[gemini]` | `GOOGLE_API_KEY` | Runs the terms-of-service evaluator with a Gemini function-calling loop. | +| `gemini_wallet_check.py` | `finance/wallet_screening` | Gemini | `[finance_wallet_screening]`, `[gemini]` | `GOOGLE_API_KEY`, `ETHERSCAN_API_KEY` | Screens an Ethereum wallet with Gemini orchestration and Etherscan data. | +| `gemini_evm_tx_handler.py` | `defi/evm_tx_handler` | Gemini | `[defi_evm_tx_handler]`, `[gemini]` | `GOOGLE_API_KEY`; for live swaps also `AGENT_WALLET_PRIVATE_KEY`, `BASE_RPC_URL` or `ETHEREUM_RPC_URL`. Set `EVM_TX_HANDLER_EXAMPLE_DEMO=1` for mocked flow without keys. | Resolve → quote → preview → execute buy flow via Gemini tool loop (or demo mode). | +| `claude_evm_tx_handler.py` | `defi/evm_tx_handler` | Claude | `[defi_evm_tx_handler]`, `[claude]` | `ANTHROPIC_API_KEY`; for live swaps also `AGENT_WALLET_PRIVATE_KEY`, RPC URLs. Demo: `EVM_TX_HANDLER_EXAMPLE_DEMO=1`. | Claude tool loop for structured DeFi intent and optional execute after confirmation. | +| `mica_claude_flow.py` | `compliance/mica_module` | Claude | `[compliance_mica_module]`, `[claude]` | `ANTHROPIC_API_KEY` | Runs a MiCA compliance agent loop through Claude. | +| `mica_ollama_flow.py` | `compliance/mica_module` | Ollama | `[compliance_mica_module]`; install `ollama` separately | None | Runs a local Ollama MiCA flow with prompt-mode tool calling. | +| `mica_rag_flow.py` | `compliance/mica_module` | Gemini | `[compliance_mica_module]`, `[gemini]` | `GOOGLE_API_KEY` | Runs the MiCA RAG flow with Gemini. | +| `ollama_skills_test.py` | `finance/wallet_screening`, `office/pdf_form_filler`, `optimization/prompt_rewriter` | Ollama | `[finance_wallet_screening]`, `[office_pdf_form_filler]`, `[optimization_prompt_rewriter]`; install `ollama` separately | `ETHERSCAN_API_KEY`, `ANTHROPIC_API_KEY` | Loads multiple skills and tests prompt-mode tool calling with Ollama. | +| `ollama_tos_evaluator.py` | `compliance/tos_evaluator` | Ollama | `[compliance_tos_evaluator]`; install `ollama` separately | None | Runs the terms-of-service evaluator with local Ollama prompt-mode calls. | +| `openai_tos_evaluator.py` | `compliance/tos_evaluator` | OpenAI | `[compliance_tos_evaluator]`, `[openai]` | `OPENAI_API_KEY` | Runs the terms-of-service evaluator with OpenAI function calling. | | `openai_compatible_host.py` | `compliance/tos_evaluator` | Groq (OpenAI-compatible) | `[compliance_tos_evaluator]`, `[openai]` | `GROQ_API_KEY`, `GROQ_MODEL` | Runs the terms-of-service evaluator through Groq's OpenAI-compatible API. | -| `pii_guardrail_flow.py` | `compliance/pii_masker` | Local execute | `[compliance_pii_masker]` | None | Demonstrates local PII masking before passing text to an external agent. | -| `prompt_compression_demo.py` | `optimization/prompt_rewriter` | Local execute | `[optimization_prompt_rewriter]` | None | Demonstrates prompt compression without a provider loop. | -| `novelty_extractor_demo.py` | `data_engineering/novelty_extractor` | Local execute | `[data_engineering_novelty_extractor]` | None | Demonstrates multi-turn corpus distillation using local embeddings with no API key. | -| `gemini_novelty_extractor.py` | `data_engineering/novelty_extractor` | Gemini | `[data_engineering_novelty_extractor]`, `[gemini]` | `GOOGLE_API_KEY` | Runs the novelty extractor with a Gemini function-calling loop. | -| `ollama_novelty_extractor.py` | `data_engineering/novelty_extractor` | Ollama | `[data_engineering_novelty_extractor]`; install `ollama` separately | None | Runs the novelty extractor with local Ollama prompt-mode calls. | -| `gemini_issue_resolver.py` | `dev_tools/issue_resolver` | Gemini | `[dev_tools_issue_resolver]`, `[gemini]` | `GOOGLE_API_KEY`; optional `GITHUB_TOKEN` | Gemini loop for GitHub issue analysis; fetches issue data after `prepare` (sample: issue #123). | -| `ollama_issue_resolver.py` | `dev_tools/issue_resolver` | Ollama | `[dev_tools_issue_resolver]`; install `ollama` separately | optional `GITHUB_TOKEN`; `OLLAMA_MODEL` (default `gemma4:e2b`) | Ollama prompt-mode loop for GitHub issue analysis (sample: issue #123). | -| `token_limiter_loop.py` | `monitoring/token_limiter` | Local execute | `[monitoring_token_limiter]` | None | Simulates a runaway task hitting a token ceiling with deterministic budget checks. | -| `gemini_token_limiter.py` | `monitoring/token_limiter` | Gemini | `[monitoring_token_limiter]`, `[gemini]` | Optional `GOOGLE_API_KEY` for Phase 2 live loop | Local budget simulation plus optional Gemini tool loop. | -| `claude_token_limiter.py` | `monitoring/token_limiter` | Claude | `[monitoring_token_limiter]`, `[claude]` | Optional `ANTHROPIC_API_KEY` for Phase 2 live loop | Local budget simulation plus optional Claude tool loop. | -| `gemini_uk_companies_house_handler.py` | `finance/uk_companies_house_handler` | Gemini | `[finance_uk_companies_house_handler]`, `[gemini]` | `GOOGLE_API_KEY`, `COMPANIES_HOUSE_API_KEY` | Resolve company, officers, filings via Gemini tool loop. | -| `uk_companies_house_handler_demo.py` | `finance/uk_companies_house_handler` | Local execute | `[finance_uk_companies_house_handler]` | None | Runs a scripted sequence (resolve, profile, officers, pscs, filings) with mocked HTTP responses (no API keys needed). | - -## Notes - -- **Skill extras** are listed per script above. Empty extras (`[]` in `pyproject.toml`) still install core + skillware; use them so docs stay stable when a skill gains new runtime deps. -- Agent-side model keys such as `GOOGLE_API_KEY`, `ANTHROPIC_API_KEY`, - `OPENAI_API_KEY`, and `DEEPSEEK_API_KEY` are documented in the provider - guides. -- Skill runtime keys such as `ETHERSCAN_API_KEY` are documented in each skill - manifest and on the skill catalog pages. -- Ollama examples require the Python `ollama` package, a local Ollama server, - and the model named in the script, but no cloud API key. +| `pii_guardrail_flow.py` | `compliance/pii_masker` | Local execute | `[compliance_pii_masker]` | None | Demonstrates local PII masking before passing text to an external agent. | +| `prompt_compression_demo.py` | `optimization/prompt_rewriter` | Local execute | `[optimization_prompt_rewriter]` | None | Demonstrates prompt compression without a provider loop. | +| `novelty_extractor_demo.py` | `data_engineering/novelty_extractor` | Local execute | `[data_engineering_novelty_extractor]` | None | Demonstrates multi-turn corpus distillation using local embeddings with no API key. | +| `gemini_novelty_extractor.py` | `data_engineering/novelty_extractor` | Gemini | `[data_engineering_novelty_extractor]`, `[gemini]` | `GOOGLE_API_KEY` | Runs the novelty extractor with a Gemini function-calling loop. | +| `ollama_novelty_extractor.py` | `data_engineering/novelty_extractor` | Ollama | `[data_engineering_novelty_extractor]`; install `ollama` separately | None | Runs the novelty extractor with local Ollama prompt-mode calls. | +| `gemini_issue_resolver.py` | `dev_tools/issue_resolver` | Gemini | `[dev_tools_issue_resolver]`, `[gemini]` | `GOOGLE_API_KEY`; optional `GITHUB_TOKEN` | Gemini loop for GitHub issue analysis; fetches issue data after `prepare` (sample: issue #123). | +| `ollama_issue_resolver.py` | `dev_tools/issue_resolver` | Ollama | `[dev_tools_issue_resolver]`; install `ollama` separately | optional `GITHUB_TOKEN`; `OLLAMA_MODEL` (default `gemma4:e2b`) | Ollama prompt-mode loop for GitHub issue analysis (sample: issue #123). | +| `token_limiter_loop.py` | `monitoring/token_limiter` | Local execute | `[monitoring_token_limiter]` | None | Simulates a runaway task hitting a token ceiling with deterministic budget checks. | +| `gemini_token_limiter.py` | `monitoring/token_limiter` | Gemini | `[monitoring_token_limiter]`, `[gemini]` | Optional `GOOGLE_API_KEY` for Phase 2 live loop | Local budget simulation plus optional Gemini tool loop. | +| `claude_token_limiter.py` | `monitoring/token_limiter` | Claude | `[monitoring_token_limiter]`, `[claude]` | Optional `ANTHROPIC_API_KEY` for Phase 2 live loop | Local budget simulation plus optional Claude tool loop. | +| `gemini_uk_companies_house_handler.py` | `finance/uk_companies_house_handler` | Gemini | `[finance_uk_companies_house_handler]`, `[gemini]` | `GOOGLE_API_KEY`, `COMPANIES_HOUSE_API_KEY` | Resolve company, officers, filings via Gemini tool loop. | +| `uk_companies_house_handler_demo.py` | `finance/uk_companies_house_handler` | Local execute | `[finance_uk_companies_house_handler]` | None | Runs a scripted sequence (resolve, profile, officers, pscs, filings) with mocked HTTP responses (no API keys needed). | +| `bg_remover_demo.py` | `creative/bg_remover` | Local execute | `[creative_bg_remover]` | None | Demonstrates offline background removal from a local image and optionally writes a transparent PNG. | +## Notes + +- **Skill extras** are listed per script above. Empty extras (`[]` in `pyproject.toml`) still install core + skillware; use them so docs stay stable when a skill gains new runtime deps. +- Agent-side model keys such as `GOOGLE_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, and `DEEPSEEK_API_KEY` are documented in the provider guides. +- Skill runtime keys such as `ETHERSCAN_API_KEY` are documented in each skill manifest and on the skill catalog pages. +- Ollama examples require the Python `ollama` package, a local Ollama server, and the model named in the script, but no cloud API key. diff --git a/examples/bg_remover_demo.py b/examples/bg_remover_demo.py new file mode 100644 index 0000000..e2d9901 --- /dev/null +++ b/examples/bg_remover_demo.py @@ -0,0 +1,41 @@ +from pathlib import Path + +from skillware.core.loader import SkillLoader + + +def run_demo(): + print("Loading Background Remover...") + + skill_bundle = SkillLoader.load_skill("creative/bg_remover") + skill_instance = skill_bundle["class"]() + + input_path = "examples/sample_input.png" + output_path = "examples/sample_output_no_bg.png" + + if not Path(input_path).exists(): + print(f"Input image not found: {input_path}") + print("Place a sample PNG at the above path before running this demo.") + return + + print(f"Removing background from: {input_path}") + + result = skill_instance.execute( + { + "input_path": input_path, + "output_path": output_path, + } + ) + + if result["success"]: + print("Background removed successfully!") + print(f"Saved to: {result['output_path']}") + print(f"Output size: {result['width']} x {result['height']}") + print(f"Model: {result['model_used']}") + print(f"Base64 length: {len(result['image_base64'])}") + else: + print(f"Failed: {result['error']}") + print(f"Error code: {result['error_code']}") + + +if __name__ == "__main__": + run_demo() diff --git a/skills/creative/bg_remover/instructions.md b/skills/creative/bg_remover/instructions.md index ee0e3c6..1846732 100644 --- a/skills/creative/bg_remover/instructions.md +++ b/skills/creative/bg_remover/instructions.md @@ -32,6 +32,8 @@ For S3, GCS, Azure Blob, or Cloudflare R2: download the object to a temp file On the **first run** in a fresh environment, processing may take a few minutes while rembg downloads the ONNX model (~176 MB for `isnet-general-use`). Later runs reuse the cache and are much faster. +Background removal sessions are also reused across executions for the same model, reducing repeated initialization overhead. + ## Input (one required: `image` OR `input_path`) | Scenario | Parameter | @@ -43,6 +45,15 @@ On the **first run** in a fresh environment, processing may take a few minutes w If **both** `image` and `input_path` are sent, **`image` wins** (do not double-submit). +### Input validation + +- Invalid Base64 payloads are rejected. +- `input_path` must reference an existing file (directories are rejected). +- Empty image files are rejected. +- Images larger than **25 MB** are rejected. +- Image integrity is validated before processing. +- output_path must not contain path traversal components (for example `..`). + ### Example payloads Chat / attachment: @@ -91,6 +102,8 @@ Omit `model` for default `isnet-general-use`. Set `alpha_matting` only when edge | Chat or API only | Omit `output_path`; use `image_base64` from the result (always present on success). | | Save next to original | Same directory, new name (e.g. `1223_no_bg.png`). | +If the parent directory of `output_path` does not exist, it is created automatically before writing the PNG. + ## Interpreting a successful result When `success` is `true`: @@ -109,10 +122,13 @@ Runtime: `rembg`, `pillow`, `onnxruntime`. Install: `pip install "skillware[crea First `execute()` downloads the ONNX model to the rembg cache (`~/.u2net/` on Linux/macOS, `%USERPROFILE%\.u2net\` on Windows). +Subsequent executions reuse cached rembg sessions for the selected model, reducing repeated initialization overhead. + ## Errors | `error_code` | Response | | :--- | :--- | -| `INVALID_INPUT` | Ask for an upload, attachment, base64 payload, or local `input_path`. | +| `INVALID_INPUT` | Invalid Base64, missing input, directory path, empty image, oversized image (>25 MB), corrupt image, or unsupported input. | | `MISSING_DEPENDENCY` | Ask the user to run `pip install "skillware[creative_bg_remover]"` (or `pip install rembg pillow onnxruntime`), then retry. | | `PROCESSING_FAILED` | Surface the `error` string; input may be missing, corrupt, or unsupported. | +| `FILE_NOT_FOUND` | The supplied `input_path` does not exist. Ask the user to verify the path and try again. | diff --git a/skills/creative/bg_remover/manifest.yaml b/skills/creative/bg_remover/manifest.yaml index 3e59e81..c4e61c9 100644 --- a/skills/creative/bg_remover/manifest.yaml +++ b/skills/creative/bg_remover/manifest.yaml @@ -1,5 +1,5 @@ name: "creative/bg_remover" -version: "0.1.0" +version: "0.2.0" description: "Remove image backgrounds locally using rembg." short_description: "Offline background removal using rembg with transparent PNG output." @@ -77,7 +77,7 @@ outputs: error_code: type: string - description: "INVALID_INPUT, MISSING_DEPENDENCY, or PROCESSING_FAILED." + description: "INVALID_INPUT, FILE_NOT_FOUND, MISSING_DEPENDENCY, or PROCESSING_FAILED." requirements: - rembg>=2.0.0 diff --git a/skills/creative/bg_remover/skill.py b/skills/creative/bg_remover/skill.py index ceac48e..953ca96 100644 --- a/skills/creative/bg_remover/skill.py +++ b/skills/creative/bg_remover/skill.py @@ -5,15 +5,38 @@ from skillware.core.base_skill import BaseSkill +MAX_IMAGE_BYTES = 25 * 1024 * 1024 # 25 MB + class BackgroundRemover(BaseSkill): """Remove image backgrounds locally using rembg.""" + _sessions = {} + + @classmethod + def _get_session(cls, model: str): + """Load and reuse rembg sessions across executions.""" + if model not in cls._sessions: + from rembg import new_session + + cls._sessions[model] = new_session(model) + + return cls._sessions[model] + + @staticmethod + def _validate_output_path(output_path: str) -> Path: + output = Path(output_path) + + if ".." in output.parts: + raise ValueError("Unsafe output_path contains path traversal.") + + return output + @property def manifest(self) -> Dict[str, Any]: return { "name": "creative/bg_remover", - "version": "0.1.0", + "version": "0.2.0", "description": ( "Remove image backgrounds locally using rembg " "and return a transparent PNG." @@ -26,7 +49,7 @@ def execute(self, params: Dict[str, Any]) -> Dict[str, Any]: try: try: from PIL import Image - from rembg import new_session, remove + from rembg import remove except ImportError: return { "success": False, @@ -53,11 +76,61 @@ def execute(self, params: Dict[str, Any]) -> Dict[str, Any]: # Read image bytes if image_b64: - image_bytes = base64.b64decode(image_b64) + try: + image_bytes = base64.b64decode(image_b64, validate=True) + except Exception: + return { + "success": False, + "error": "Invalid base64 image.", + "error_code": "INVALID_INPUT", + } else: - image_bytes = Path(input_path).read_bytes() + input_file = Path(input_path) + + if input_file.is_dir(): + return { + "success": False, + "error": "Input path must be a file, not a directory.", + "error_code": "INVALID_INPUT", + } + + if not input_file.exists(): + return { + "success": False, + "error": f"Input file '{input_path}' was not found.", + "error_code": "FILE_NOT_FOUND", + } + + image_bytes = input_file.read_bytes() + + if len(image_bytes) > MAX_IMAGE_BYTES: + return { + "success": False, + "error": f"Input image exceeds the maximum size of {MAX_IMAGE_BYTES // (1024 * 1024)} MB.", + "error_code": "INVALID_INPUT", + } + + if len(image_bytes) == 0: + return { + "success": False, + "error": "Input image is empty.", + "error_code": "INVALID_INPUT", + } + + try: + image = Image.open(io.BytesIO(image_bytes)) + image.verify() + + # Reopen after verify() because verify() leaves the image unusable. + image = Image.open(io.BytesIO(image_bytes)) + except Exception: + return { + "success": False, + "error": "Input is not a valid image.", + "error_code": "INVALID_INPUT", + } - session = new_session(model) + session = self._get_session(model) output_bytes = remove( image_bytes, @@ -76,7 +149,17 @@ def execute(self, params: Dict[str, Any]) -> Dict[str, Any]: # Optional save if output_path: - Path(output_path).write_bytes(buffer.getvalue()) + try: + output_file = self._validate_output_path(output_path) + except ValueError: + return { + "success": False, + "error": "Unsafe output_path.", + "error_code": "INVALID_INPUT", + } + + output_file.parent.mkdir(parents=True, exist_ok=True) + output_file.write_bytes(buffer.getvalue()) return { "success": True, diff --git a/skills/creative/bg_remover/test_skill.py b/skills/creative/bg_remover/test_skill.py index 92f50fb..9d141e2 100644 --- a/skills/creative/bg_remover/test_skill.py +++ b/skills/creative/bg_remover/test_skill.py @@ -9,7 +9,7 @@ from skillware.core.loader import SkillLoader -from .skill import BackgroundRemover +from .skill import BackgroundRemover, MAX_IMAGE_BYTES import sys import types @@ -117,7 +117,7 @@ def test_invalid_base64(skill): result = skill.execute({"image": "not_base64"}) assert result["success"] is False - assert result["error_code"] == "PROCESSING_FAILED" + assert result["error_code"] == "INVALID_INPUT" def test_missing_dependency(monkeypatch, skill): @@ -249,4 +249,101 @@ def test_missing_input_path(skill): result = skill.execute({"input_path": "/nonexistent/path/image.png"}) assert result["success"] is False - assert result["error_code"] == "PROCESSING_FAILED" + assert result["error_code"] == "FILE_NOT_FOUND" + + +def test_directory_input(skill, tmp_path): + result = skill.execute({"input_path": str(tmp_path)}) + + assert result["success"] is False + assert result["error_code"] == "INVALID_INPUT" + + +def test_empty_input_file(skill, tmp_path): + empty = tmp_path / "empty.png" + empty.write_bytes(b"") + + result = skill.execute({"input_path": str(empty)}) + + assert result["success"] is False + assert result["error_code"] == "INVALID_INPUT" + + +def test_invalid_image_file(skill, tmp_path): + bad = tmp_path / "bad.png" + bad.write_text("not an image") + + result = skill.execute({"input_path": str(bad)}) + + assert result["success"] is False + assert result["error_code"] == "INVALID_INPUT" + + +def test_output_directory_created(skill, tmp_path): + image = Image.new("RGB", (64, 64), "white") + + input_file = tmp_path / "input.png" + image.save(input_file) + + output_file = tmp_path / "nested" / "folder" / "output.png" + + result = skill.execute( + { + "input_path": str(input_file), + "output_path": str(output_file), + } + ) + + assert result["success"] is True + assert output_file.exists() + + +def test_session_reuse(skill, monkeypatch): + calls = {"count": 0} + + def fake_remove(image_bytes, *args, **kwargs): + img = Image.new("RGBA", (100, 100), (255, 0, 0, 0)) + buffer = io.BytesIO() + img.save(buffer, format="PNG") + return buffer.getvalue() + + def fake_new_session(model_name="u2net", *args, **kwargs): + calls["count"] += 1 + return object() + + fake_module = types.ModuleType("rembg") + fake_module.remove = fake_remove + fake_module.new_session = fake_new_session + fake_module.__spec__ = importlib.util.spec_from_loader("rembg", loader=None) + + monkeypatch.setitem(sys.modules, "rembg", fake_module) + + BackgroundRemover._sessions.clear() + + skill.execute({"image": create_image()}) + skill.execute({"image": create_image()}) + + assert calls["count"] == 1 + + BackgroundRemover._sessions.clear() + + +def test_unsafe_output_path(skill): + result = skill.execute( + { + "image": create_image(), + "output_path": "../../../evil.png", + } + ) + + assert result["success"] is False + assert result["error_code"] == "INVALID_INPUT" + + +def test_oversized_base64_image(skill): + oversized = base64.b64encode(b"\0" * (MAX_IMAGE_BYTES + 1)).decode() + + result = skill.execute({"image": oversized}) + + assert result["success"] is False + assert result["error_code"] == "INVALID_INPUT"