feat(skills): include uploads directory in skill scanning
This commit is contained in:
parent
66bdc951f8
commit
5aa38ee108
|
|
@ -394,7 +394,17 @@ def get_skills_prompt_section(available_skills: set[str] | None = None) -> str:
|
||||||
Returns the <skill_system>...</skill_system> block listing all enabled skills,
|
Returns the <skill_system>...</skill_system> block listing all enabled skills,
|
||||||
suitable for injection into any agent's system prompt.
|
suitable for injection into any agent's system prompt.
|
||||||
"""
|
"""
|
||||||
skills = _get_enabled_skills()
|
thread_id = None
|
||||||
|
try:
|
||||||
|
from langgraph.config import get_config
|
||||||
|
|
||||||
|
config = get_config()
|
||||||
|
thread_id = config.get("configurable", {}).get("thread_id")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Keep regular enable/disable behavior while letting uploads be default-enabled in loader.
|
||||||
|
skills = load_skills(enabled_only=True, thread_id=thread_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from deerflow.config import get_app_config
|
from deerflow.config import get_app_config
|
||||||
|
|
@ -561,4 +571,6 @@ def apply_prompt_template(subagent_enabled: bool = False, max_concurrent_subagen
|
||||||
acp_section=acp_and_mounts_section,
|
acp_section=acp_and_mounts_section,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.debug("Generated full system prompt:\n%s", prompt)
|
||||||
|
|
||||||
return prompt + f"\n<current_date>{datetime.now().strftime('%Y-%m-%d, %A')}</current_date>"
|
return prompt + f"\n<current_date>{datetime.now().strftime('%Y-%m-%d, %A')}</current_date>"
|
||||||
|
|
|
||||||
|
|
@ -192,8 +192,8 @@ class ExtensionsConfig(BaseModel):
|
||||||
"""
|
"""
|
||||||
skill_config = self.skills.get(skill_name)
|
skill_config = self.skills.get(skill_name)
|
||||||
if skill_config is None:
|
if skill_config is None:
|
||||||
# Default to enable for public & custom skill
|
# Default to enable for public/custom/uploads skills.
|
||||||
return skill_category in ("public", "custom")
|
return skill_category in ("public", "custom", "uploads")
|
||||||
return skill_config.enabled
|
return skill_config.enabled
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,27 @@ from .types import Skill
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
UPLOADS_SKILLS_PATH = Path("/mnt/user-data/uploads")
|
||||||
|
|
||||||
|
|
||||||
|
def get_uploads_skills_path(thread_id: str | None = None) -> Path:
|
||||||
|
"""Resolve the uploads skills root for the current execution context.
|
||||||
|
|
||||||
|
When called from the LangGraph process, uploaded skills live under the
|
||||||
|
host-side per-thread data directory rather than the sandbox mount path.
|
||||||
|
"""
|
||||||
|
if not thread_id:
|
||||||
|
return UPLOADS_SKILLS_PATH
|
||||||
|
|
||||||
|
try:
|
||||||
|
from deerflow.config.paths import get_paths
|
||||||
|
|
||||||
|
return get_paths().sandbox_uploads_dir(thread_id)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Failed to resolve uploads skills path for thread %s: %s", thread_id, exc)
|
||||||
|
return UPLOADS_SKILLS_PATH
|
||||||
|
|
||||||
|
|
||||||
def get_skills_root_path() -> Path:
|
def get_skills_root_path() -> Path:
|
||||||
"""
|
"""
|
||||||
Get the root path of the skills directory.
|
Get the root path of the skills directory.
|
||||||
|
|
@ -22,12 +43,19 @@ def get_skills_root_path() -> Path:
|
||||||
return skills_dir
|
return skills_dir
|
||||||
|
|
||||||
|
|
||||||
def load_skills(skills_path: Path | None = None, use_config: bool = True, enabled_only: bool = False) -> list[Skill]:
|
def load_skills(
|
||||||
|
skills_path: Path | None = None,
|
||||||
|
use_config: bool = True,
|
||||||
|
enabled_only: bool = False,
|
||||||
|
thread_id: str | None = None,
|
||||||
|
) -> list[Skill]:
|
||||||
"""
|
"""
|
||||||
Load all skills from the skills directory.
|
Load all skills from the skills directory.
|
||||||
|
|
||||||
Scans both public and custom skill directories, parsing SKILL.md files
|
Scans public/custom skill directories under the configured skills root,
|
||||||
to extract metadata. The enabled state is determined by the skills_state_config.json file.
|
and also scans uploaded skills under /mnt/user-data/uploads.
|
||||||
|
SKILL.md metadata is parsed and enabled state is derived from
|
||||||
|
skills_state_config.json.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
skills_path: Optional custom path to skills directory.
|
skills_path: Optional custom path to skills directory.
|
||||||
|
|
@ -35,6 +63,8 @@ def load_skills(skills_path: Path | None = None, use_config: bool = True, enable
|
||||||
Otherwise defaults to deer-flow/skills
|
Otherwise defaults to deer-flow/skills
|
||||||
use_config: Whether to load skills path from config (default: True)
|
use_config: Whether to load skills path from config (default: True)
|
||||||
enabled_only: If True, only return enabled skills (default: False)
|
enabled_only: If True, only return enabled skills (default: False)
|
||||||
|
thread_id: Optional thread ID used to resolve per-thread uploads skills
|
||||||
|
from the LangGraph host process
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of Skill objects, sorted by name
|
List of Skill objects, sorted by name
|
||||||
|
|
@ -57,12 +87,22 @@ def load_skills(skills_path: Path | None = None, use_config: bool = True, enable
|
||||||
|
|
||||||
skills = []
|
skills = []
|
||||||
|
|
||||||
# Scan public and custom directories
|
# Scan built-in roots and uploaded skills mounted in personal workspace.
|
||||||
for category in ["public", "custom"]:
|
scan_targets: list[tuple[str, Path]] = [
|
||||||
category_path = skills_path / category
|
("public", skills_path / "public"),
|
||||||
|
("custom", skills_path / "custom"),
|
||||||
|
("uploads", get_uploads_skills_path(thread_id)),
|
||||||
|
]
|
||||||
|
|
||||||
|
for category, category_path in scan_targets:
|
||||||
|
logger.debug("Scanning %s skills under %s", category, category_path)
|
||||||
|
|
||||||
if not category_path.exists() or not category_path.is_dir():
|
if not category_path.exists() or not category_path.is_dir():
|
||||||
|
logger.debug("Skip %s scan: directory not found or not a directory (%s)", category, category_path)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
scanned_skill_dirs: list[str] = []
|
||||||
|
|
||||||
for current_root, dir_names, file_names in os.walk(category_path, followlinks=True):
|
for current_root, dir_names, file_names in os.walk(category_path, followlinks=True):
|
||||||
# Keep traversal deterministic and skip hidden directories.
|
# Keep traversal deterministic and skip hidden directories.
|
||||||
dir_names[:] = sorted(name for name in dir_names if not name.startswith("."))
|
dir_names[:] = sorted(name for name in dir_names if not name.startswith("."))
|
||||||
|
|
@ -71,11 +111,22 @@ def load_skills(skills_path: Path | None = None, use_config: bool = True, enable
|
||||||
|
|
||||||
skill_file = Path(current_root) / "SKILL.md"
|
skill_file = Path(current_root) / "SKILL.md"
|
||||||
relative_path = skill_file.parent.relative_to(category_path)
|
relative_path = skill_file.parent.relative_to(category_path)
|
||||||
|
scanned_skill_dirs.append(relative_path.as_posix())
|
||||||
|
|
||||||
skill = parse_skill_file(skill_file, category=category, relative_path=relative_path)
|
skill = parse_skill_file(skill_file, category=category, relative_path=relative_path)
|
||||||
if skill:
|
if skill:
|
||||||
skills.append(skill)
|
skills.append(skill)
|
||||||
|
|
||||||
|
if scanned_skill_dirs:
|
||||||
|
logger.debug(
|
||||||
|
"%s scan found %d skill directories: %s",
|
||||||
|
category,
|
||||||
|
len(scanned_skill_dirs),
|
||||||
|
", ".join(sorted(scanned_skill_dirs)),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.debug("%s scan found no skill directories", category)
|
||||||
|
|
||||||
# Load skills state configuration and update enabled status
|
# Load skills state configuration and update enabled status
|
||||||
# NOTE: We use ExtensionsConfig.from_file() instead of get_extensions_config()
|
# NOTE: We use ExtensionsConfig.from_file() instead of get_extensions_config()
|
||||||
# to always read the latest configuration from disk. This ensures that changes
|
# to always read the latest configuration from disk. This ensures that changes
|
||||||
|
|
@ -86,6 +137,10 @@ def load_skills(skills_path: Path | None = None, use_config: bool = True, enable
|
||||||
|
|
||||||
extensions_config = ExtensionsConfig.from_file()
|
extensions_config = ExtensionsConfig.from_file()
|
||||||
for skill in skills:
|
for skill in skills:
|
||||||
|
if skill.category == "uploads":
|
||||||
|
# Uploaded skills should be available by default for the current thread.
|
||||||
|
skill.enabled = True
|
||||||
|
continue
|
||||||
skill.enabled = extensions_config.is_skill_enabled(skill.name, skill.category)
|
skill.enabled = extensions_config.is_skill_enabled(skill.name, skill.category)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# If config loading fails, default to all enabled
|
# If config loading fails, default to all enabled
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ def parse_skill_file(skill_file: Path, category: str, relative_path: Path | None
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
skill_file: Path to the SKILL.md file
|
skill_file: Path to the SKILL.md file
|
||||||
category: Category of the skill ('public' or 'custom')
|
category: Category of the skill ('public', 'custom', or 'uploads')
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Skill object if parsing succeeds, None otherwise
|
Skill object if parsing succeeds, None otherwise
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ class Skill:
|
||||||
skill_dir: Path
|
skill_dir: Path
|
||||||
skill_file: Path
|
skill_file: Path
|
||||||
relative_path: Path # Relative path from category root to skill directory
|
relative_path: Path # Relative path from category root to skill directory
|
||||||
category: str # 'public' or 'custom'
|
category: str # 'public', 'custom', or 'uploads'
|
||||||
enabled: bool = False # Whether this skill is enabled
|
enabled: bool = False # Whether this skill is enabled
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -31,7 +31,10 @@ class Skill:
|
||||||
Returns:
|
Returns:
|
||||||
Full container path to the skill directory
|
Full container path to the skill directory
|
||||||
"""
|
"""
|
||||||
category_base = f"{container_base_path}/{self.category}"
|
if self.category == "uploads":
|
||||||
|
category_base = "/mnt/user-data/uploads"
|
||||||
|
else:
|
||||||
|
category_base = f"{container_base_path}/{self.category}"
|
||||||
skill_path = self.skill_path
|
skill_path = self.skill_path
|
||||||
if skill_path:
|
if skill_path:
|
||||||
return f"{category_base}/{skill_path}"
|
return f"{category_base}/{skill_path}"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue