feat(skills): include uploads directory in skill scanning
This commit is contained in:
parent
867bb6de46
commit
e517ee7938
|
|
@ -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,
|
||||
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:
|
||||
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,
|
||||
)
|
||||
|
||||
logger.debug("Generated full system prompt:\n%s", prompt)
|
||||
|
||||
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)
|
||||
if skill_config is None:
|
||||
# Default to enable for public & custom skill
|
||||
return skill_category in ("public", "custom")
|
||||
# Default to enable for public/custom/uploads skills.
|
||||
return skill_category in ("public", "custom", "uploads")
|
||||
return skill_config.enabled
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,27 @@ from .types import Skill
|
|||
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:
|
||||
"""
|
||||
Get the root path of the skills directory.
|
||||
|
|
@ -22,12 +43,19 @@ def get_skills_root_path() -> Path:
|
|||
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.
|
||||
|
||||
Scans both public and custom skill directories, parsing SKILL.md files
|
||||
to extract metadata. The enabled state is determined by the skills_state_config.json file.
|
||||
Scans public/custom skill directories under the configured skills root,
|
||||
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:
|
||||
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
|
||||
use_config: Whether to load skills path from config (default: True)
|
||||
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:
|
||||
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 = []
|
||||
|
||||
# Scan public and custom directories
|
||||
for category in ["public", "custom"]:
|
||||
category_path = skills_path / category
|
||||
# Scan built-in roots and uploaded skills mounted in personal workspace.
|
||||
scan_targets: list[tuple[str, Path]] = [
|
||||
("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():
|
||||
logger.debug("Skip %s scan: directory not found or not a directory (%s)", category, category_path)
|
||||
continue
|
||||
|
||||
scanned_skill_dirs: list[str] = []
|
||||
|
||||
for current_root, dir_names, file_names in os.walk(category_path, followlinks=True):
|
||||
# Keep traversal deterministic and skip hidden directories.
|
||||
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"
|
||||
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)
|
||||
if 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
|
||||
# NOTE: We use ExtensionsConfig.from_file() instead of get_extensions_config()
|
||||
# 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()
|
||||
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)
|
||||
except Exception as e:
|
||||
# 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:
|
||||
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:
|
||||
Skill object if parsing succeeds, None otherwise
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class Skill:
|
|||
skill_dir: Path
|
||||
skill_file: Path
|
||||
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
|
||||
|
||||
@property
|
||||
|
|
@ -31,7 +31,10 @@ class Skill:
|
|||
Returns:
|
||||
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
|
||||
if skill_path:
|
||||
return f"{category_base}/{skill_path}"
|
||||
|
|
|
|||
Loading…
Reference in New Issue