diff --git a/README.md b/README.md
index 2ee85c9..af5c9ba 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,50 @@
> Originated from Open Source, give back to Open Source.
+A LangGraph-based AI agent backend with sandbox execution capabilities.
+
+## Quick Start
+
+1. **Configure the application**:
+ ```bash
+ # Copy example configuration
+ cp config.example.yaml config.yaml
+
+ # Set your API keys
+ export OPENAI_API_KEY="your-key-here"
+ # or edit config.yaml directly
+ ```
+
+2. **Install dependencies**:
+ ```bash
+ cd backend
+ make install
+ ```
+
+3. **Run development server**:
+ ```bash
+ make dev
+ ```
+
+## Project Structure
+
+```
+deer-flow/
+├── config.example.yaml # Configuration template (copy to config.yaml)
+├── backend/ # Backend application
+│ ├── src/ # Source code
+│ └── docs/ # Documentation
+├── frontend/ # Frontend application
+└── skills/ # Agent skills
+ ├── public/ # Public skills
+ └── custom/ # Custom skills
+```
+
+## Documentation
+
+- [Configuration Guide](backend/docs/CONFIGURATION.md) - Setup and configuration instructions
+- [Architecture Overview](backend/CLAUDE.md) - Technical architecture details
+
## License
This project is open source and available under the [MIT License](./LICENSE).
diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md
index 104afd5..98e837e 100644
--- a/backend/CLAUDE.md
+++ b/backend/CLAUDE.md
@@ -26,11 +26,20 @@ make format
### Configuration System
-The app uses a YAML-based configuration system loaded from `config.yaml`. Configuration priority:
+The app uses a YAML-based configuration system loaded from `config.yaml`.
+
+**Setup**: Copy `config.example.yaml` to `config.yaml` in the **project root** directory and customize for your environment.
+
+```bash
+# From project root (deer-flow/)
+cp config.example.yaml config.yaml
+```
+
+Configuration priority:
1. Explicit `config_path` argument
2. `DEER_FLOW_CONFIG_PATH` environment variable
-3. `config.yaml` in current directory
-4. `config.yaml` in parent directory
+3. `config.yaml` in current directory (backend/)
+4. `config.yaml` in parent directory (project root - **recommended location**)
Config values starting with `$` are resolved as environment variables (e.g., `$OPENAI_API_KEY`).
@@ -61,12 +70,25 @@ Config values starting with `$` are resolved as environment variables (e.g., `$O
- `resolve_variable()` imports module and returns variable (e.g., `module:variable`)
- `resolve_class()` imports and validates class against base class
+**Skills System** (`src/skills/`)
+- Skills provide specialized workflows for specific tasks (e.g., PDF processing, frontend design)
+- Located in `deer-flow/skills/{public,custom}` directory structure
+- Each skill has a `SKILL.md` file with YAML front matter (name, description, license)
+- Skills are automatically discovered and loaded at runtime
+- `load_skills()` scans directories and parses SKILL.md files
+- Skills are injected into agent's system prompt with paths
+- Path mapping system allows seamless access in both local and Docker sandbox:
+ - Local sandbox: `/mnt/skills` → `/path/to/deer-flow/skills`
+ - Docker sandbox: Automatically mounted as volume
+
### Config Schema
-Models, tools, and sandbox providers are configured in `config.yaml`:
+Models, tools, sandbox providers, and skills are configured in `config.yaml`:
- `models[]`: LLM configurations with `use` class path
- `tools[]`: Tool configurations with `use` variable path and `group`
- `sandbox.use`: Sandbox provider class path
+- `skills.path`: Host path to skills directory (optional, default: `../skills`)
+- `skills.container_path`: Container mount path (default: `/mnt/skills`)
## Code Style
diff --git a/backend/SETUP.md b/backend/SETUP.md
new file mode 100644
index 0000000..411268b
--- /dev/null
+++ b/backend/SETUP.md
@@ -0,0 +1,76 @@
+# Setup Guide
+
+Quick setup instructions for DeerFlow.
+
+## Configuration Setup
+
+DeerFlow uses a YAML configuration file that should be placed in the **project root directory**.
+
+### Steps
+
+1. **Navigate to project root**:
+ ```bash
+ cd /path/to/deer-flow
+ ```
+
+2. **Copy example configuration**:
+ ```bash
+ cp config.example.yaml config.yaml
+ ```
+
+3. **Edit configuration**:
+ ```bash
+ # Option A: Set environment variables (recommended)
+ export OPENAI_API_KEY="your-key-here"
+
+ # Option B: Edit config.yaml directly
+ vim config.yaml # or your preferred editor
+ ```
+
+4. **Verify configuration**:
+ ```bash
+ cd backend
+ python -c "from src.config import get_app_config; print('✓ Config loaded:', get_app_config().models[0].name)"
+ ```
+
+## Important Notes
+
+- **Location**: `config.yaml` should be in `deer-flow/` (project root), not `deer-flow/backend/`
+- **Git**: `config.yaml` is automatically ignored by git (contains secrets)
+- **Priority**: If both `backend/config.yaml` and `../config.yaml` exist, backend version takes precedence
+
+## Configuration File Locations
+
+The backend searches for `config.yaml` in this order:
+
+1. `DEER_FLOW_CONFIG_PATH` environment variable (if set)
+2. `backend/config.yaml` (current directory when running from backend/)
+3. `deer-flow/config.yaml` (parent directory - **recommended location**)
+
+**Recommended**: Place `config.yaml` in project root (`deer-flow/config.yaml`).
+
+## Troubleshooting
+
+### Config file not found
+
+```bash
+# Check where the backend is looking
+cd deer-flow/backend
+python -c "from src.config.app_config import AppConfig; print(AppConfig.resolve_config_path())"
+```
+
+If it can't find the config:
+1. Ensure you've copied `config.example.yaml` to `config.yaml`
+2. Verify you're in the correct directory
+3. Check the file exists: `ls -la ../config.yaml`
+
+### Permission denied
+
+```bash
+chmod 600 ../config.yaml # Protect sensitive configuration
+```
+
+## See Also
+
+- [Configuration Guide](docs/CONFIGURATION.md) - Detailed configuration options
+- [Architecture Overview](CLAUDE.md) - System architecture
diff --git a/backend/docs/CONFIGURATION.md b/backend/docs/CONFIGURATION.md
new file mode 100644
index 0000000..93fba86
--- /dev/null
+++ b/backend/docs/CONFIGURATION.md
@@ -0,0 +1,221 @@
+# Configuration Guide
+
+This guide explains how to configure DeerFlow for your environment.
+
+## Quick Start
+
+1. **Copy the example configuration** (from project root):
+ ```bash
+ # From project root directory (deer-flow/)
+ cp config.example.yaml config.yaml
+ ```
+
+2. **Set your API keys**:
+
+ Option A: Use environment variables (recommended):
+ ```bash
+ export OPENAI_API_KEY="your-api-key-here"
+ export ANTHROPIC_API_KEY="your-api-key-here"
+ # Add other keys as needed
+ ```
+
+ Option B: Edit `config.yaml` directly (not recommended for production):
+ ```yaml
+ models:
+ - name: gpt-4
+ api_key: your-actual-api-key-here # Replace placeholder
+ ```
+
+3. **Start the application**:
+ ```bash
+ make dev
+ ```
+
+## Configuration Sections
+
+### Models
+
+Configure the LLM models available to the agent:
+
+```yaml
+models:
+ - name: gpt-4 # Internal identifier
+ display_name: GPT-4 # Human-readable name
+ use: langchain_openai:ChatOpenAI # LangChain class path
+ model: gpt-4 # Model identifier for API
+ api_key: $OPENAI_API_KEY # API key (use env var)
+ max_tokens: 4096 # Max tokens per request
+ temperature: 0.7 # Sampling temperature
+```
+
+**Supported Providers**:
+- OpenAI (`langchain_openai:ChatOpenAI`)
+- Anthropic (`langchain_anthropic:ChatAnthropic`)
+- DeepSeek (`langchain_deepseek:ChatDeepSeek`)
+- Any LangChain-compatible provider
+
+**Thinking Models**:
+Some models support "thinking" mode for complex reasoning:
+
+```yaml
+models:
+ - name: deepseek-v3
+ supports_thinking: true
+ when_thinking_enabled:
+ extra_body:
+ thinking:
+ type: enabled
+```
+
+### Tool Groups
+
+Organize tools into logical groups:
+
+```yaml
+tool_groups:
+ - name: web # Web browsing and search
+ - name: file:read # Read-only file operations
+ - name: file:write # Write file operations
+ - name: bash # Shell command execution
+```
+
+### Tools
+
+Configure specific tools available to the agent:
+
+```yaml
+tools:
+ - name: web_search
+ group: web
+ use: src.community.tavily.tools:web_search_tool
+ max_results: 5
+ # api_key: $TAVILY_API_KEY # Optional
+```
+
+**Built-in Tools**:
+- `web_search` - Search the web (Tavily)
+- `web_fetch` - Fetch web pages (Jina AI)
+- `ls` - List directory contents
+- `read_file` - Read file contents
+- `write_file` - Write file contents
+- `str_replace` - String replacement in files
+- `bash` - Execute bash commands
+
+### Sandbox
+
+Choose between local execution or Docker-based isolation:
+
+**Option 1: Local Sandbox** (default, simpler setup):
+```yaml
+sandbox:
+ use: src.sandbox.local:LocalSandboxProvider
+```
+
+**Option 2: Docker Sandbox** (isolated, more secure):
+```yaml
+sandbox:
+ use: src.community.aio_sandbox:AioSandboxProvider
+ port: 8080
+ auto_start: true
+ container_prefix: deer-flow-sandbox
+
+ # Optional: Additional mounts
+ mounts:
+ - host_path: /path/on/host
+ container_path: /path/in/container
+ read_only: false
+```
+
+### Skills
+
+Configure the skills directory for specialized workflows:
+
+```yaml
+skills:
+ # Host path (optional, default: ../skills)
+ path: /custom/path/to/skills
+
+ # Container mount path (default: /mnt/skills)
+ container_path: /mnt/skills
+```
+
+**How Skills Work**:
+- Skills are stored in `deer-flow/skills/{public,custom}/`
+- Each skill has a `SKILL.md` file with metadata
+- Skills are automatically discovered and loaded
+- Available in both local and Docker sandbox via path mapping
+
+### Title Generation
+
+Automatic conversation title generation:
+
+```yaml
+title:
+ enabled: true
+ max_words: 6
+ max_chars: 60
+ model_name: null # Use first model in list
+```
+
+## Environment Variables
+
+DeerFlow supports environment variable substitution using the `$` prefix:
+
+```yaml
+models:
+ - api_key: $OPENAI_API_KEY # Reads from environment
+```
+
+**Common Environment Variables**:
+- `OPENAI_API_KEY` - OpenAI API key
+- `ANTHROPIC_API_KEY` - Anthropic API key
+- `DEEPSEEK_API_KEY` - DeepSeek API key
+- `TAVILY_API_KEY` - Tavily search API key
+- `DEER_FLOW_CONFIG_PATH` - Custom config file path
+
+## Configuration Location
+
+The configuration file should be placed in the **project root directory** (`deer-flow/config.yaml`), not in the backend directory.
+
+## Configuration Priority
+
+DeerFlow searches for configuration in this order:
+
+1. Path specified in code via `config_path` argument
+2. Path from `DEER_FLOW_CONFIG_PATH` environment variable
+3. `config.yaml` in current working directory (typically `backend/` when running)
+4. `config.yaml` in parent directory (project root: `deer-flow/`)
+
+## Best Practices
+
+1. **Place `config.yaml` in project root** - Not in `backend/` directory
+2. **Never commit `config.yaml`** - It's already in `.gitignore`
+3. **Use environment variables for secrets** - Don't hardcode API keys
+4. **Keep `config.example.yaml` updated** - Document all new options
+5. **Test configuration changes locally** - Before deploying
+6. **Use Docker sandbox for production** - Better isolation and security
+
+## Troubleshooting
+
+### "Config file not found"
+- Ensure `config.yaml` exists in the **project root** directory (`deer-flow/config.yaml`)
+- The backend searches parent directory by default, so root location is preferred
+- Alternatively, set `DEER_FLOW_CONFIG_PATH` environment variable to custom location
+
+### "Invalid API key"
+- Verify environment variables are set correctly
+- Check that `$` prefix is used for env var references
+
+### "Skills not loading"
+- Check that `deer-flow/skills/` directory exists
+- Verify skills have valid `SKILL.md` files
+- Check `skills.path` configuration if using custom path
+
+### "Docker sandbox fails to start"
+- Ensure Docker is running
+- Check port 8080 (or configured port) is available
+- Verify Docker image is accessible
+
+## Examples
+
+See `config.example.yaml` for complete examples of all configuration options.
diff --git a/backend/src/agents/lead_agent/prompt.py b/backend/src/agents/lead_agent/prompt.py
index adfa588..9dc8af5 100644
--- a/backend/src/agents/lead_agent/prompt.py
+++ b/backend/src/agents/lead_agent/prompt.py
@@ -1,6 +1,8 @@
from datetime import datetime
-SYSTEM_PROMPT = f"""
+from src.skills import load_skills
+
+SYSTEM_PROMPT_TEMPLATE = """
You are DeerFlow 2.0, an open-source super agent.
@@ -14,19 +16,16 @@ You are DeerFlow 2.0, an open-source super agent.
You have access to skills that provide optimized workflows for specific tasks. Each skill contains best practices, frameworks, and references to additional resources.
**Progressive Loading Pattern:**
-1. When a user query matches a skill's use case, immediately call `view` on the skill's main file located at `/mnt/skills/{"{skill_name}"}/SKILL.md`
+1. When a user query matches a skill's use case, immediately call `view` on the skill's main file using the path attribute provided in the skill tag below
2. Read and understand the skill's workflow and instructions
3. The skill file contains references to external resources under the same folder
4. Load referenced resources only when needed during execution
5. Follow the skill's instructions precisely
+**Skills are located at:** {skills_base_path}
+
-
-Generate a web page or web application
-
-
-Extract text, fill forms, merge PDFs (pypdf, pdfplumber)
-
+{skills_list}
@@ -64,4 +63,27 @@ All temporary work happens in `/mnt/user-data/workspace`. Final deliverables mus
def apply_prompt_template() -> str:
- return SYSTEM_PROMPT + f"\n{datetime.now().strftime('%Y-%m-%d, %A')}"
+ # Load all available skills
+ skills = load_skills()
+
+ # Get skills container path from config
+ try:
+ from src.config import get_app_config
+
+ config = get_app_config()
+ container_base_path = config.skills.container_path
+ except Exception:
+ # Fallback to default if config fails
+ container_base_path = "/mnt/skills"
+
+ # Generate skills list XML with paths
+ skills_list = "\n".join(f'\n{skill.description}\n' for skill in skills)
+
+ # If no skills found, provide empty list
+ if not skills_list:
+ skills_list = ""
+
+ # Format the prompt with dynamic skills
+ prompt = SYSTEM_PROMPT_TEMPLATE.format(skills_list=skills_list, skills_base_path=container_base_path)
+
+ return prompt + f"\n{datetime.now().strftime('%Y-%m-%d, %A')}"
diff --git a/backend/src/community/aio_sandbox/aio_sandbox_provider.py b/backend/src/community/aio_sandbox/aio_sandbox_provider.py
index 38d6bcc..47a6345 100644
--- a/backend/src/community/aio_sandbox/aio_sandbox_provider.py
+++ b/backend/src/community/aio_sandbox/aio_sandbox_provider.py
@@ -100,6 +100,26 @@ class AioSandboxProvider(SandboxProvider):
(str(thread_dir / "outputs"), f"{CONTAINER_USER_DATA_DIR}/outputs", False),
]
+ def _get_skills_mount(self) -> tuple[str, str, bool] | None:
+ """Get the skills directory mount configuration.
+
+ Returns:
+ Tuple of (host_path, container_path, read_only) if skills directory exists,
+ None otherwise.
+ """
+ try:
+ config = get_app_config()
+ skills_path = config.skills.get_skills_path()
+ container_path = config.skills.container_path
+
+ # Only mount if skills directory exists
+ if skills_path.exists():
+ return (str(skills_path), container_path, True) # Read-only mount for security
+ except Exception as e:
+ logger.warning(f"Could not setup skills mount: {e}")
+
+ return None
+
def _start_container(self, sandbox_id: str, port: int, extra_mounts: list[tuple[str, str, bool]] | None = None) -> str:
"""Start a new Docker container for the sandbox.
@@ -208,11 +228,17 @@ class AioSandboxProvider(SandboxProvider):
sandbox_id = str(uuid.uuid4())[:8]
# Get thread-specific mounts if thread_id is provided
- extra_mounts = None
+ extra_mounts = []
if thread_id:
- extra_mounts = self._get_thread_mounts(thread_id)
+ extra_mounts.extend(self._get_thread_mounts(thread_id))
logger.info(f"Adding thread mounts for thread {thread_id}: {extra_mounts}")
+ # Add skills mount if available
+ skills_mount = self._get_skills_mount()
+ if skills_mount:
+ extra_mounts.append(skills_mount)
+ logger.info(f"Adding skills mount: {skills_mount}")
+
# If base_url is configured, use existing sandbox
if self._config.get("base_url"):
base_url = self._config["base_url"]
@@ -230,7 +256,7 @@ class AioSandboxProvider(SandboxProvider):
raise RuntimeError("auto_start is disabled and no base_url is configured")
port = self._find_available_port(self._config["port"])
- container_id = self._start_container(sandbox_id, port, extra_mounts=extra_mounts)
+ container_id = self._start_container(sandbox_id, port, extra_mounts=extra_mounts if extra_mounts else None)
self._containers[sandbox_id] = container_id
base_url = f"http://localhost:{port}"
diff --git a/backend/src/config/__init__.py b/backend/src/config/__init__.py
index f9d546a..e46ffca 100644
--- a/backend/src/config/__init__.py
+++ b/backend/src/config/__init__.py
@@ -1,3 +1,4 @@
from .app_config import get_app_config
+from .skills_config import SkillsConfig
-__all__ = ["get_app_config"]
+__all__ = ["get_app_config", "SkillsConfig"]
diff --git a/backend/src/config/app_config.py b/backend/src/config/app_config.py
index de34cdd..2d7b546 100644
--- a/backend/src/config/app_config.py
+++ b/backend/src/config/app_config.py
@@ -8,6 +8,7 @@ from pydantic import BaseModel, ConfigDict, Field
from src.config.model_config import ModelConfig
from src.config.sandbox_config import SandboxConfig
+from src.config.skills_config import SkillsConfig
from src.config.title_config import load_title_config_from_dict
from src.config.tool_config import ToolConfig, ToolGroupConfig
@@ -21,6 +22,7 @@ class AppConfig(BaseModel):
sandbox: SandboxConfig = Field(description="Sandbox configuration")
tools: list[ToolConfig] = Field(default_factory=list, description="Available tools")
tool_groups: list[ToolGroupConfig] = Field(default_factory=list, description="Available tool groups")
+ skills: SkillsConfig = Field(default_factory=SkillsConfig, description="Skills configuration")
model_config = ConfigDict(extra="allow", frozen=False)
@classmethod
diff --git a/backend/src/config/skills_config.py b/backend/src/config/skills_config.py
new file mode 100644
index 0000000..18876f7
--- /dev/null
+++ b/backend/src/config/skills_config.py
@@ -0,0 +1,49 @@
+from pathlib import Path
+
+from pydantic import BaseModel, Field
+
+
+class SkillsConfig(BaseModel):
+ """Configuration for skills system"""
+
+ path: str | None = Field(
+ default=None,
+ description="Path to skills directory. If not specified, defaults to ../skills relative to backend directory",
+ )
+ container_path: str = Field(
+ default="/mnt/skills",
+ description="Path where skills are mounted in the sandbox container",
+ )
+
+ def get_skills_path(self) -> Path:
+ """
+ Get the resolved skills directory path.
+
+ Returns:
+ Path to the skills directory
+ """
+ if self.path:
+ # Use configured path (can be absolute or relative)
+ path = Path(self.path)
+ if not path.is_absolute():
+ # If relative, resolve from current working directory
+ path = Path.cwd() / path
+ return path.resolve()
+ else:
+ # Default: ../skills relative to backend directory
+ from src.skills.loader import get_skills_root_path
+
+ return get_skills_root_path()
+
+ def get_skill_container_path(self, skill_name: str, category: str = "public") -> str:
+ """
+ Get the full container path for a specific skill.
+
+ Args:
+ skill_name: Name of the skill (directory name)
+ category: Category of the skill (public or custom)
+
+ Returns:
+ Full path to the skill in the container
+ """
+ return f"{self.container_path}/{category}/{skill_name}"
diff --git a/backend/src/sandbox/local/local_sandbox.py b/backend/src/sandbox/local/local_sandbox.py
index 3eecf27..27376ae 100644
--- a/backend/src/sandbox/local/local_sandbox.py
+++ b/backend/src/sandbox/local/local_sandbox.py
@@ -1,13 +1,46 @@
import os
import subprocess
+from pathlib import Path
from src.sandbox.local.list_dir import list_dir
from src.sandbox.sandbox import Sandbox
class LocalSandbox(Sandbox):
- def __init__(self, id: str):
+ def __init__(self, id: str, path_mappings: dict[str, str] | None = None):
+ """
+ Initialize local sandbox with optional path mappings.
+
+ Args:
+ id: Sandbox identifier
+ path_mappings: Dictionary mapping container paths to local paths
+ Example: {"/mnt/skills": "/absolute/path/to/skills"}
+ """
super().__init__(id)
+ self.path_mappings = path_mappings or {}
+
+ def _resolve_path(self, path: str) -> str:
+ """
+ Resolve container path to actual local path using mappings.
+
+ Args:
+ path: Path that might be a container path
+
+ Returns:
+ Resolved local path
+ """
+ path_str = str(path)
+
+ # Try each mapping (longest prefix first for more specific matches)
+ for container_path, local_path in sorted(self.path_mappings.items(), key=lambda x: len(x[0]), reverse=True):
+ if path_str.startswith(container_path):
+ # Replace the container path prefix with local path
+ relative = path_str[len(container_path) :].lstrip("/")
+ resolved = str(Path(local_path) / relative) if relative else local_path
+ return resolved
+
+ # No mapping found, return original path
+ return path_str
def execute_command(self, command: str) -> str:
result = subprocess.run(
@@ -26,16 +59,19 @@ class LocalSandbox(Sandbox):
return output if output else "(no output)"
def list_dir(self, path: str, max_depth=2) -> list[str]:
- return list_dir(path, max_depth)
+ resolved_path = self._resolve_path(path)
+ return list_dir(resolved_path, max_depth)
def read_file(self, path: str) -> str:
- with open(path) as f:
+ resolved_path = self._resolve_path(path)
+ with open(resolved_path) as f:
return f.read()
def write_file(self, path: str, content: str, append: bool = False) -> None:
- dir_path = os.path.dirname(path)
+ resolved_path = self._resolve_path(path)
+ dir_path = os.path.dirname(resolved_path)
if dir_path:
os.makedirs(dir_path, exist_ok=True)
mode = "a" if append else "w"
- with open(path, mode) as f:
+ with open(resolved_path, mode) as f:
f.write(content)
diff --git a/backend/src/sandbox/local/local_sandbox_provider.py b/backend/src/sandbox/local/local_sandbox_provider.py
index bb96098..162c1b6 100644
--- a/backend/src/sandbox/local/local_sandbox_provider.py
+++ b/backend/src/sandbox/local/local_sandbox_provider.py
@@ -5,10 +5,42 @@ _singleton: LocalSandbox | None = None
class LocalSandboxProvider(SandboxProvider):
+ def __init__(self):
+ """Initialize the local sandbox provider with path mappings."""
+ self._path_mappings = self._setup_path_mappings()
+
+ def _setup_path_mappings(self) -> dict[str, str]:
+ """
+ Setup path mappings for local sandbox.
+
+ Maps container paths to actual local paths, including skills directory.
+
+ Returns:
+ Dictionary of path mappings
+ """
+ mappings = {}
+
+ # Map skills container path to local skills directory
+ try:
+ from src.config import get_app_config
+
+ config = get_app_config()
+ skills_path = config.skills.get_skills_path()
+ container_path = config.skills.container_path
+
+ # Only add mapping if skills directory exists
+ if skills_path.exists():
+ mappings[container_path] = str(skills_path)
+ except Exception as e:
+ # Log but don't fail if config loading fails
+ print(f"Warning: Could not setup skills path mapping: {e}")
+
+ return mappings
+
def acquire(self, thread_id: str | None = None) -> str:
global _singleton
if _singleton is None:
- _singleton = LocalSandbox("local")
+ _singleton = LocalSandbox("local", path_mappings=self._path_mappings)
return _singleton.id
def get(self, sandbox_id: str) -> None:
diff --git a/backend/src/skills/__init__.py b/backend/src/skills/__init__.py
new file mode 100644
index 0000000..f051298
--- /dev/null
+++ b/backend/src/skills/__init__.py
@@ -0,0 +1,4 @@
+from .loader import get_skills_root_path, load_skills
+from .types import Skill
+
+__all__ = ["load_skills", "get_skills_root_path", "Skill"]
diff --git a/backend/src/skills/loader.py b/backend/src/skills/loader.py
new file mode 100644
index 0000000..e6bd8bb
--- /dev/null
+++ b/backend/src/skills/loader.py
@@ -0,0 +1,77 @@
+from pathlib import Path
+
+from .parser import parse_skill_file
+from .types import Skill
+
+
+def get_skills_root_path() -> Path:
+ """
+ Get the root path of the skills directory.
+
+ Returns:
+ Path to the skills directory (deer-flow/skills)
+ """
+ # backend directory is current file's parent's parent's parent
+ backend_dir = Path(__file__).resolve().parent.parent.parent
+ # skills directory is sibling to backend directory
+ skills_dir = backend_dir.parent / "skills"
+ return skills_dir
+
+
+def load_skills(skills_path: Path | None = None, use_config: bool = True) -> list[Skill]:
+ """
+ Load all skills from the skills directory.
+
+ Scans both public and custom skill directories, parsing SKILL.md files
+ to extract metadata.
+
+ Args:
+ skills_path: Optional custom path to skills directory.
+ If not provided and use_config is True, uses path from config.
+ Otherwise defaults to deer-flow/skills
+ use_config: Whether to load skills path from config (default: True)
+
+ Returns:
+ List of Skill objects, sorted by name
+ """
+ if skills_path is None:
+ if use_config:
+ try:
+ from src.config import get_app_config
+
+ config = get_app_config()
+ skills_path = config.skills.get_skills_path()
+ except Exception:
+ # Fallback to default if config fails
+ skills_path = get_skills_root_path()
+ else:
+ skills_path = get_skills_root_path()
+
+ if not skills_path.exists():
+ return []
+
+ skills = []
+
+ # Scan public and custom directories
+ for category in ["public", "custom"]:
+ category_path = skills_path / category
+ if not category_path.exists() or not category_path.is_dir():
+ continue
+
+ # Each subdirectory is a potential skill
+ for skill_dir in category_path.iterdir():
+ if not skill_dir.is_dir():
+ continue
+
+ skill_file = skill_dir / "SKILL.md"
+ if not skill_file.exists():
+ continue
+
+ skill = parse_skill_file(skill_file, category=category)
+ if skill:
+ skills.append(skill)
+
+ # Sort by name for consistent ordering
+ skills.sort(key=lambda s: s.name)
+
+ return skills
diff --git a/backend/src/skills/parser.py b/backend/src/skills/parser.py
new file mode 100644
index 0000000..069cdf9
--- /dev/null
+++ b/backend/src/skills/parser.py
@@ -0,0 +1,63 @@
+import re
+from pathlib import Path
+
+from .types import Skill
+
+
+def parse_skill_file(skill_file: Path, category: str) -> Skill | None:
+ """
+ Parse a SKILL.md file and extract metadata.
+
+ Args:
+ skill_file: Path to the SKILL.md file
+ category: Category of the skill ('public' or 'custom')
+
+ Returns:
+ Skill object if parsing succeeds, None otherwise
+ """
+ if not skill_file.exists() or skill_file.name != "SKILL.md":
+ return None
+
+ try:
+ content = skill_file.read_text(encoding="utf-8")
+
+ # Extract YAML front matter
+ # Pattern: ---\nkey: value\n---
+ front_matter_match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)
+
+ if not front_matter_match:
+ return None
+
+ front_matter = front_matter_match.group(1)
+
+ # Parse YAML front matter (simple key-value parsing)
+ metadata = {}
+ for line in front_matter.split("\n"):
+ line = line.strip()
+ if not line:
+ continue
+ if ":" in line:
+ key, value = line.split(":", 1)
+ metadata[key.strip()] = value.strip()
+
+ # Extract required fields
+ name = metadata.get("name")
+ description = metadata.get("description")
+
+ if not name or not description:
+ return None
+
+ license_text = metadata.get("license")
+
+ return Skill(
+ name=name,
+ description=description,
+ license=license_text,
+ skill_dir=skill_file.parent,
+ skill_file=skill_file,
+ category=category,
+ )
+
+ except Exception as e:
+ print(f"Error parsing skill file {skill_file}: {e}")
+ return None
diff --git a/backend/src/skills/types.py b/backend/src/skills/types.py
new file mode 100644
index 0000000..9d46654
--- /dev/null
+++ b/backend/src/skills/types.py
@@ -0,0 +1,34 @@
+from dataclasses import dataclass
+from pathlib import Path
+
+
+@dataclass
+class Skill:
+ """Represents a skill with its metadata and file path"""
+
+ name: str
+ description: str
+ license: str | None
+ skill_dir: Path
+ skill_file: Path
+ category: str # 'public' or 'custom'
+
+ @property
+ def skill_path(self) -> str:
+ """Returns the relative path from skills root to this skill's directory"""
+ return self.skill_dir.name
+
+ def get_container_path(self, container_base_path: str = "/mnt/skills") -> str:
+ """
+ Get the full path to this skill in the container.
+
+ Args:
+ container_base_path: Base path where skills are mounted in the container
+
+ Returns:
+ Full container path to the skill directory
+ """
+ return f"{container_base_path}/{self.category}/{self.skill_dir.name}"
+
+ def __repr__(self) -> str:
+ return f"Skill(name={self.name!r}, description={self.description!r}, category={self.category!r})"
diff --git a/config.example.yaml b/config.example.yaml
index 20a5d18..23a8964 100644
--- a/config.example.yaml
+++ b/config.example.yaml
@@ -1,30 +1,65 @@
# Configuration for the DeerFlow application
#
# Guidelines:
-# - The default path of this configuration file is `config.yaml` in the CWD (Current Working Directory) or the parent directory of the CWD.
-# How ever you can change it using the `DEER_FLOW_CONFIG_PATH` environment variable.
+# - Copy this file to `config.yaml` and customize it for your environment
+# - The default path of this configuration file is `config.yaml` in the current working directory.
+# However you can change it using the `DEER_FLOW_CONFIG_PATH` environment variable.
# - Environment variables are available for all field values. Example: `api_key: $OPENAI_API_KEY`
-# - Provider path is a string that looks like "package_name.sub_package_name.module_name:class_name/variable_name".
+# - The `use` path is a string that looks like "package_name.sub_package_name.module_name:class_name/variable_name".
+
+# ============================================================================
+# Models Configuration
+# ============================================================================
+# Configure available LLM models for the agent to use
models:
- - name: doubao-seed-1.8
- display_name: Doubao 1.8
- use: langchain_deepseek:ChatDeepSeek
- model: doubao-seed-1-8-251228
- api_base: https://ark.cn-beijing.volces.com/api/v3
- api_key: $ARK_API_KEY
- supports_thinking: true
- when_thinking_enabled:
- extra_body:
- thinking:
- type: enabled
- - name: gpt-5
- display_name: GPT-5
+ # Example: OpenAI model
+ - name: gpt-4
+ display_name: GPT-4
use: langchain_openai:ChatOpenAI
- model: gpt-5-251228
- api_base: https://api.openai.com/v1
- api_key: $OPENAI_API_KEY
- supports_thinking: true
+ model: gpt-4
+ api_key: $OPENAI_API_KEY # Use environment variable
+ max_tokens: 4096
+ temperature: 0.7
+
+ # Example: Anthropic Claude model
+ # - name: claude-3-5-sonnet
+ # display_name: Claude 3.5 Sonnet
+ # use: langchain_anthropic:ChatAnthropic
+ # model: claude-3-5-sonnet-20241022
+ # api_key: $ANTHROPIC_API_KEY
+ # max_tokens: 8192
+
+ # Example: DeepSeek model (with thinking support)
+ # - name: deepseek-v3
+ # display_name: DeepSeek V3 (Thinking)
+ # use: langchain_deepseek:ChatDeepSeek
+ # model: deepseek-chat
+ # api_key: $DEEPSEEK_API_KEY
+ # max_tokens: 16384
+ # supports_thinking: true
+ # when_thinking_enabled:
+ # extra_body:
+ # thinking:
+ # type: enabled
+
+ # Example: Volcengine (Doubao) model
+ # - name: doubao-seed-1.8
+ # display_name: Doubao 1.8 (Thinking)
+ # use: langchain_deepseek:ChatDeepSeek
+ # model: ep-m-20260106111913-xxxxx
+ # api_base: https://ark.cn-beijing.volces.com/api/v3
+ # api_key: $VOLCENGINE_API_KEY
+ # supports_thinking: true
+ # when_thinking_enabled:
+ # extra_body:
+ # thinking:
+ # type: enabled
+
+# ============================================================================
+# Tool Groups Configuration
+# ============================================================================
+# Define groups of tools for organization and access control
tool_groups:
- name: web
@@ -32,17 +67,26 @@ tool_groups:
- name: file:write
- name: bash
+# ============================================================================
+# Tools Configuration
+# ============================================================================
+# Configure available tools for the agent to use
+
tools:
+ # Web search tool (requires Tavily API key)
- name: web_search
group: web
use: src.community.tavily.tools:web_search_tool
max_results: 5
+ # api_key: $TAVILY_API_KEY # Set if needed
+ # Web fetch tool (uses Jina AI reader)
- name: web_fetch
group: web
use: src.community.jina_ai.tools:web_fetch_tool
timeout: 10
+ # File operations tools
- name: ls
group: file:read
use: src.sandbox.tools:ls_tool
@@ -59,37 +103,74 @@ tools:
group: file:write
use: src.sandbox.tools:str_replace_tool
+ # Bash execution tool
- name: bash
group: bash
use: src.sandbox.tools:bash_tool
+
+# ============================================================================
+# Sandbox Configuration
+# ============================================================================
+# Choose between local sandbox (direct execution) or Docker-based AIO sandbox
+
+# Option 1: Local Sandbox (Default)
+# Executes commands directly on the host machine
sandbox:
use: src.sandbox.local:LocalSandboxProvider
-# To use Docker-based AIO sandbox instead, uncomment the following:
+# Option 2: Docker-based AIO Sandbox
+# Executes commands in isolated Docker containers
+# Uncomment to use:
# sandbox:
# use: src.community.aio_sandbox:AioSandboxProvider
+#
# # Optional: Use existing sandbox at this URL (no Docker container will be started)
# # base_url: http://localhost:8080
-# # Optional: Docker image to use (default: enterprise-public-cn-beijing.cr.volces.com/vefaas-public/all-in-one-sandbox:latest)
+#
+# # Optional: Docker image to use
+# # Default: enterprise-public-cn-beijing.cr.volces.com/vefaas-public/all-in-one-sandbox:latest
# # image: enterprise-public-cn-beijing.cr.volces.com/vefaas-public/all-in-one-sandbox:latest
+#
# # Optional: Base port for sandbox containers (default: 8080)
# # port: 8080
+#
# # Optional: Whether to automatically start Docker container (default: true)
# # auto_start: true
+#
# # Optional: Prefix for container names (default: deer-flow-sandbox)
# # container_prefix: deer-flow-sandbox
-# # Optional: Mount directories from host to container
+#
+# # Optional: Additional mount directories from host to container
+# # NOTE: Skills directory is automatically mounted from skills.path to skills.container_path
# # mounts:
+# # # Other custom mounts
# # - host_path: /path/on/host
# # container_path: /home/user/shared
# # read_only: false
-# # - host_path: /another/path
-# # container_path: /data
-# # read_only: true
-# Automatic thread title generation
+# ============================================================================
+# Skills Configuration
+# ============================================================================
+# Configure skills directory for specialized agent workflows
+
+skills:
+ # Path to skills directory on the host (relative to project root or absolute)
+ # Default: ../skills (relative to backend directory)
+ # Uncomment to customize:
+ # path: /absolute/path/to/custom/skills
+
+ # Path where skills are mounted in the sandbox container
+ # This is used by the agent to access skills in both local and Docker sandbox
+ # Default: /mnt/skills
+ container_path: /mnt/skills
+
+# ============================================================================
+# Title Generation Configuration
+# ============================================================================
+# Automatic conversation title generation settings
+
title:
enabled: true
max_words: 6
max_chars: 60
- model_name: null # Use default model
\ No newline at end of file
+ model_name: null # Use default model (first model in models list)
diff --git a/skills/public/frontend-design/LICENSE.txt b/skills/public/frontend-design/LICENSE.txt
new file mode 100644
index 0000000..f433b1a
--- /dev/null
+++ b/skills/public/frontend-design/LICENSE.txt
@@ -0,0 +1,177 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/skills/public/frontend-design/SKILL.md b/skills/public/frontend-design/SKILL.md
new file mode 100644
index 0000000..5be498e
--- /dev/null
+++ b/skills/public/frontend-design/SKILL.md
@@ -0,0 +1,42 @@
+---
+name: frontend-design
+description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
+license: Complete terms in LICENSE.txt
+---
+
+This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
+
+The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
+
+## Design Thinking
+
+Before coding, understand the context and commit to a BOLD aesthetic direction:
+- **Purpose**: What problem does this interface solve? Who uses it?
+- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
+- **Constraints**: Technical requirements (framework, performance, accessibility).
+- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
+
+**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
+
+Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
+- Production-grade and functional
+- Visually striking and memorable
+- Cohesive with a clear aesthetic point-of-view
+- Meticulously refined in every detail
+
+## Frontend Aesthetics Guidelines
+
+Focus on:
+- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
+- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
+- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
+- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
+- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
+
+NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
+
+Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
+
+**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
+
+Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
diff --git a/skills/public/pdf-processing/LICENSE.txt b/skills/public/pdf-processing/LICENSE.txt
new file mode 100644
index 0000000..c55ab42
--- /dev/null
+++ b/skills/public/pdf-processing/LICENSE.txt
@@ -0,0 +1,30 @@
+© 2025 Anthropic, PBC. All rights reserved.
+
+LICENSE: Use of these materials (including all code, prompts, assets, files,
+and other components of this Skill) is governed by your agreement with
+Anthropic regarding use of Anthropic's services. If no separate agreement
+exists, use is governed by Anthropic's Consumer Terms of Service or
+Commercial Terms of Service, as applicable:
+https://www.anthropic.com/legal/consumer-terms
+https://www.anthropic.com/legal/commercial-terms
+Your applicable agreement is referred to as the "Agreement." "Services" are
+as defined in the Agreement.
+
+ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the
+contrary, users may not:
+
+- Extract these materials from the Services or retain copies of these
+ materials outside the Services
+- Reproduce or copy these materials, except for temporary copies created
+ automatically during authorized use of the Services
+- Create derivative works based on these materials
+- Distribute, sublicense, or transfer these materials to any third party
+- Make, offer to sell, sell, or import any inventions embodied in these
+ materials
+- Reverse engineer, decompile, or disassemble these materials
+
+The receipt, viewing, or possession of these materials does not convey or
+imply any license or right beyond those expressly granted above.
+
+Anthropic retains all right, title, and interest in these materials,
+including all copyrights, patents, and other intellectual property rights.
diff --git a/skills/public/pdf-processing/SKILL.md b/skills/public/pdf-processing/SKILL.md
new file mode 100644
index 0000000..f6a22dd
--- /dev/null
+++ b/skills/public/pdf-processing/SKILL.md
@@ -0,0 +1,294 @@
+---
+name: pdf
+description: Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale.
+license: Proprietary. LICENSE.txt has complete terms
+---
+
+# PDF Processing Guide
+
+## Overview
+
+This guide covers essential PDF processing operations using Python libraries and command-line tools. For advanced features, JavaScript libraries, and detailed examples, see reference.md. If you need to fill out a PDF form, read forms.md and follow its instructions.
+
+## Quick Start
+
+```python
+from pypdf import PdfReader, PdfWriter
+
+# Read a PDF
+reader = PdfReader("document.pdf")
+print(f"Pages: {len(reader.pages)}")
+
+# Extract text
+text = ""
+for page in reader.pages:
+ text += page.extract_text()
+```
+
+## Python Libraries
+
+### pypdf - Basic Operations
+
+#### Merge PDFs
+```python
+from pypdf import PdfWriter, PdfReader
+
+writer = PdfWriter()
+for pdf_file in ["doc1.pdf", "doc2.pdf", "doc3.pdf"]:
+ reader = PdfReader(pdf_file)
+ for page in reader.pages:
+ writer.add_page(page)
+
+with open("merged.pdf", "wb") as output:
+ writer.write(output)
+```
+
+#### Split PDF
+```python
+reader = PdfReader("input.pdf")
+for i, page in enumerate(reader.pages):
+ writer = PdfWriter()
+ writer.add_page(page)
+ with open(f"page_{i+1}.pdf", "wb") as output:
+ writer.write(output)
+```
+
+#### Extract Metadata
+```python
+reader = PdfReader("document.pdf")
+meta = reader.metadata
+print(f"Title: {meta.title}")
+print(f"Author: {meta.author}")
+print(f"Subject: {meta.subject}")
+print(f"Creator: {meta.creator}")
+```
+
+#### Rotate Pages
+```python
+reader = PdfReader("input.pdf")
+writer = PdfWriter()
+
+page = reader.pages[0]
+page.rotate(90) # Rotate 90 degrees clockwise
+writer.add_page(page)
+
+with open("rotated.pdf", "wb") as output:
+ writer.write(output)
+```
+
+### pdfplumber - Text and Table Extraction
+
+#### Extract Text with Layout
+```python
+import pdfplumber
+
+with pdfplumber.open("document.pdf") as pdf:
+ for page in pdf.pages:
+ text = page.extract_text()
+ print(text)
+```
+
+#### Extract Tables
+```python
+with pdfplumber.open("document.pdf") as pdf:
+ for i, page in enumerate(pdf.pages):
+ tables = page.extract_tables()
+ for j, table in enumerate(tables):
+ print(f"Table {j+1} on page {i+1}:")
+ for row in table:
+ print(row)
+```
+
+#### Advanced Table Extraction
+```python
+import pandas as pd
+
+with pdfplumber.open("document.pdf") as pdf:
+ all_tables = []
+ for page in pdf.pages:
+ tables = page.extract_tables()
+ for table in tables:
+ if table: # Check if table is not empty
+ df = pd.DataFrame(table[1:], columns=table[0])
+ all_tables.append(df)
+
+# Combine all tables
+if all_tables:
+ combined_df = pd.concat(all_tables, ignore_index=True)
+ combined_df.to_excel("extracted_tables.xlsx", index=False)
+```
+
+### reportlab - Create PDFs
+
+#### Basic PDF Creation
+```python
+from reportlab.lib.pagesizes import letter
+from reportlab.pdfgen import canvas
+
+c = canvas.Canvas("hello.pdf", pagesize=letter)
+width, height = letter
+
+# Add text
+c.drawString(100, height - 100, "Hello World!")
+c.drawString(100, height - 120, "This is a PDF created with reportlab")
+
+# Add a line
+c.line(100, height - 140, 400, height - 140)
+
+# Save
+c.save()
+```
+
+#### Create PDF with Multiple Pages
+```python
+from reportlab.lib.pagesizes import letter
+from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
+from reportlab.lib.styles import getSampleStyleSheet
+
+doc = SimpleDocTemplate("report.pdf", pagesize=letter)
+styles = getSampleStyleSheet()
+story = []
+
+# Add content
+title = Paragraph("Report Title", styles['Title'])
+story.append(title)
+story.append(Spacer(1, 12))
+
+body = Paragraph("This is the body of the report. " * 20, styles['Normal'])
+story.append(body)
+story.append(PageBreak())
+
+# Page 2
+story.append(Paragraph("Page 2", styles['Heading1']))
+story.append(Paragraph("Content for page 2", styles['Normal']))
+
+# Build PDF
+doc.build(story)
+```
+
+## Command-Line Tools
+
+### pdftotext (poppler-utils)
+```bash
+# Extract text
+pdftotext input.pdf output.txt
+
+# Extract text preserving layout
+pdftotext -layout input.pdf output.txt
+
+# Extract specific pages
+pdftotext -f 1 -l 5 input.pdf output.txt # Pages 1-5
+```
+
+### qpdf
+```bash
+# Merge PDFs
+qpdf --empty --pages file1.pdf file2.pdf -- merged.pdf
+
+# Split pages
+qpdf input.pdf --pages . 1-5 -- pages1-5.pdf
+qpdf input.pdf --pages . 6-10 -- pages6-10.pdf
+
+# Rotate pages
+qpdf input.pdf output.pdf --rotate=+90:1 # Rotate page 1 by 90 degrees
+
+# Remove password
+qpdf --password=mypassword --decrypt encrypted.pdf decrypted.pdf
+```
+
+### pdftk (if available)
+```bash
+# Merge
+pdftk file1.pdf file2.pdf cat output merged.pdf
+
+# Split
+pdftk input.pdf burst
+
+# Rotate
+pdftk input.pdf rotate 1east output rotated.pdf
+```
+
+## Common Tasks
+
+### Extract Text from Scanned PDFs
+```python
+# Requires: pip install pytesseract pdf2image
+import pytesseract
+from pdf2image import convert_from_path
+
+# Convert PDF to images
+images = convert_from_path('scanned.pdf')
+
+# OCR each page
+text = ""
+for i, image in enumerate(images):
+ text += f"Page {i+1}:\n"
+ text += pytesseract.image_to_string(image)
+ text += "\n\n"
+
+print(text)
+```
+
+### Add Watermark
+```python
+from pypdf import PdfReader, PdfWriter
+
+# Create watermark (or load existing)
+watermark = PdfReader("watermark.pdf").pages[0]
+
+# Apply to all pages
+reader = PdfReader("document.pdf")
+writer = PdfWriter()
+
+for page in reader.pages:
+ page.merge_page(watermark)
+ writer.add_page(page)
+
+with open("watermarked.pdf", "wb") as output:
+ writer.write(output)
+```
+
+### Extract Images
+```bash
+# Using pdfimages (poppler-utils)
+pdfimages -j input.pdf output_prefix
+
+# This extracts all images as output_prefix-000.jpg, output_prefix-001.jpg, etc.
+```
+
+### Password Protection
+```python
+from pypdf import PdfReader, PdfWriter
+
+reader = PdfReader("input.pdf")
+writer = PdfWriter()
+
+for page in reader.pages:
+ writer.add_page(page)
+
+# Add password
+writer.encrypt("userpassword", "ownerpassword")
+
+with open("encrypted.pdf", "wb") as output:
+ writer.write(output)
+```
+
+## Quick Reference
+
+| Task | Best Tool | Command/Code |
+|------|-----------|--------------|
+| Merge PDFs | pypdf | `writer.add_page(page)` |
+| Split PDFs | pypdf | One page per file |
+| Extract text | pdfplumber | `page.extract_text()` |
+| Extract tables | pdfplumber | `page.extract_tables()` |
+| Create PDFs | reportlab | Canvas or Platypus |
+| Command line merge | qpdf | `qpdf --empty --pages ...` |
+| OCR scanned PDFs | pytesseract | Convert to image first |
+| Fill PDF forms | pdf-lib or pypdf (see forms.md) | See forms.md |
+
+## Next Steps
+
+- For advanced pypdfium2 usage, see reference.md
+- For JavaScript libraries (pdf-lib), see reference.md
+- If you need to fill out a PDF form, follow the instructions in forms.md
+- For troubleshooting guides, see reference.md
diff --git a/skills/public/pdf-processing/forms.md b/skills/public/pdf-processing/forms.md
new file mode 100644
index 0000000..4e23450
--- /dev/null
+++ b/skills/public/pdf-processing/forms.md
@@ -0,0 +1,205 @@
+**CRITICAL: You MUST complete these steps in order. Do not skip ahead to writing code.**
+
+If you need to fill out a PDF form, first check to see if the PDF has fillable form fields. Run this script from this file's directory:
+ `python scripts/check_fillable_fields `, and depending on the result go to either the "Fillable fields" or "Non-fillable fields" and follow those instructions.
+
+# Fillable fields
+If the PDF has fillable form fields:
+- Run this script from this file's directory: `python scripts/extract_form_field_info.py `. It will create a JSON file with a list of fields in this format:
+```
+[
+ {
+ "field_id": (unique ID for the field),
+ "page": (page number, 1-based),
+ "rect": ([left, bottom, right, top] bounding box in PDF coordinates, y=0 is the bottom of the page),
+ "type": ("text", "checkbox", "radio_group", or "choice"),
+ },
+ // Checkboxes have "checked_value" and "unchecked_value" properties:
+ {
+ "field_id": (unique ID for the field),
+ "page": (page number, 1-based),
+ "type": "checkbox",
+ "checked_value": (Set the field to this value to check the checkbox),
+ "unchecked_value": (Set the field to this value to uncheck the checkbox),
+ },
+ // Radio groups have a "radio_options" list with the possible choices.
+ {
+ "field_id": (unique ID for the field),
+ "page": (page number, 1-based),
+ "type": "radio_group",
+ "radio_options": [
+ {
+ "value": (set the field to this value to select this radio option),
+ "rect": (bounding box for the radio button for this option)
+ },
+ // Other radio options
+ ]
+ },
+ // Multiple choice fields have a "choice_options" list with the possible choices:
+ {
+ "field_id": (unique ID for the field),
+ "page": (page number, 1-based),
+ "type": "choice",
+ "choice_options": [
+ {
+ "value": (set the field to this value to select this option),
+ "text": (display text of the option)
+ },
+ // Other choice options
+ ],
+ }
+]
+```
+- Convert the PDF to PNGs (one image for each page) with this script (run from this file's directory):
+`python scripts/convert_pdf_to_images.py `
+Then analyze the images to determine the purpose of each form field (make sure to convert the bounding box PDF coordinates to image coordinates).
+- Create a `field_values.json` file in this format with the values to be entered for each field:
+```
+[
+ {
+ "field_id": "last_name", // Must match the field_id from `extract_form_field_info.py`
+ "description": "The user's last name",
+ "page": 1, // Must match the "page" value in field_info.json
+ "value": "Simpson"
+ },
+ {
+ "field_id": "Checkbox12",
+ "description": "Checkbox to be checked if the user is 18 or over",
+ "page": 1,
+ "value": "/On" // If this is a checkbox, use its "checked_value" value to check it. If it's a radio button group, use one of the "value" values in "radio_options".
+ },
+ // more fields
+]
+```
+- Run the `fill_fillable_fields.py` script from this file's directory to create a filled-in PDF:
+`python scripts/fill_fillable_fields.py