Skip to content

Commit c0e41c0

Browse files
authored
updated the skills_manager to support both local and worksapce deployments. also removed a hash requirement in the requitements.txt (#466)
1 parent 16b9cd7 commit c0e41c0

1 file changed

Lines changed: 82 additions & 48 deletions

File tree

databricks-builder-app/server/services/skills_manager.py

Lines changed: 82 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -95,25 +95,40 @@ def get_allowed_mcp_tools(
9595
]
9696

9797

98-
# Source skills directory - check multiple locations
99-
# 1. Sibling to this app (local development): ../../databricks-skills
100-
# 2. Deployed location (Databricks Apps): ./skills at app root
101-
_DEV_SKILLS_DIR = Path(__file__).parent.parent.parent.parent / 'databricks-skills'
102-
_DEPLOYED_SKILLS_DIR = Path(__file__).parent.parent.parent / 'skills'
98+
# Skills source directories. install_skills.sh aggregates skills from
99+
# multiple repos (this repo's databricks-skills/, mlflow/skills, apx) into
100+
# the app's .claude/skills/ directory. We check several locations so that
101+
# the server works both in local development and when deployed.
102+
#
103+
# Candidate source directories (checked in priority order):
104+
# 1. .claude/skills/ inside the app — populated by install_skills.sh with
105+
# the *full* union of Databricks + MLflow + APX skills.
106+
# 2. Sibling ../../databricks-skills — the repo-local Databricks-only skills.
107+
# 3. ./skills at app root — the deployed bundle location.
108+
_APP_ROOT = Path(__file__).parent.parent.parent
109+
_INSTALLED_SKILLS_DIR = _APP_ROOT / '.claude' / 'skills'
110+
_DEV_SKILLS_DIR = _APP_ROOT.parent / 'databricks-skills'
111+
_DEPLOYED_SKILLS_DIR = _APP_ROOT / 'skills'
103112

104113
# Local cache of skills within this app (copied on startup)
105-
APP_SKILLS_DIR = Path(__file__).parent.parent.parent / 'skills'
114+
APP_SKILLS_DIR = _APP_ROOT / 'skills'
106115

107-
# Prefer dev location (sibling repo) when available to avoid self-deletion:
108-
# APP_SKILLS_DIR == _DEPLOYED_SKILLS_DIR, so using deployed as source would
109-
# delete the source during copy_skills_to_app(). Only fall back to deployed
110-
# location when the dev source repo isn't available (actual deployment).
111-
if _DEV_SKILLS_DIR.exists() and any(_DEV_SKILLS_DIR.iterdir()):
112-
SKILLS_SOURCE_DIR = _DEV_SKILLS_DIR
113-
elif _DEPLOYED_SKILLS_DIR.exists() and any(_DEPLOYED_SKILLS_DIR.iterdir()):
114-
SKILLS_SOURCE_DIR = _DEPLOYED_SKILLS_DIR
115-
else:
116-
SKILLS_SOURCE_DIR = _DEV_SKILLS_DIR
116+
def _non_empty_dir(p: Path) -> bool:
117+
return p.exists() and p.is_dir() and any(p.iterdir())
118+
119+
# Build an ordered list of source directories. The first directory that
120+
# contains a given skill wins, so put the most-complete source first.
121+
_SKILLS_SOURCE_DIRS: list[Path] = []
122+
if _non_empty_dir(_INSTALLED_SKILLS_DIR):
123+
_SKILLS_SOURCE_DIRS.append(_INSTALLED_SKILLS_DIR)
124+
if _non_empty_dir(_DEV_SKILLS_DIR):
125+
_SKILLS_SOURCE_DIRS.append(_DEV_SKILLS_DIR)
126+
if _non_empty_dir(_DEPLOYED_SKILLS_DIR) and _DEPLOYED_SKILLS_DIR.resolve() != APP_SKILLS_DIR.resolve():
127+
_SKILLS_SOURCE_DIRS.append(_DEPLOYED_SKILLS_DIR)
128+
129+
# Legacy single-directory reference used by callers that haven't been
130+
# updated yet. Points to the first available source.
131+
SKILLS_SOURCE_DIR = _SKILLS_SOURCE_DIRS[0] if _SKILLS_SOURCE_DIRS else _DEV_SKILLS_DIR
117132

118133

119134
def _get_enabled_skills() -> list[str] | None:
@@ -190,8 +205,24 @@ class SkillNotFoundError(Exception):
190205
pass
191206

192207

208+
def _find_skill_source(skill_name: str) -> Path | None:
209+
"""Find a skill directory across all source directories.
210+
211+
Returns the first directory that contains ``skill_name/SKILL.md``.
212+
"""
213+
for src_dir in _SKILLS_SOURCE_DIRS:
214+
candidate = src_dir / skill_name
215+
if candidate.is_dir() and (candidate / 'SKILL.md').exists():
216+
return candidate
217+
return None
218+
219+
193220
def copy_skills_to_app() -> bool:
194-
"""Copy skills from source repo to app's skills directory.
221+
"""Copy skills from source directories to app's skills directory.
222+
223+
Skills may originate from multiple locations (the repo's databricks-skills/,
224+
the .claude/skills/ directory populated by install_skills.sh, or the
225+
deployed skills bundle). This function merges them into APP_SKILLS_DIR.
195226
196227
Called on server startup to ensure we have the latest skills.
197228
Only copies skills listed in ENABLED_SKILLS env var (if set).
@@ -200,68 +231,71 @@ def copy_skills_to_app() -> bool:
200231
True if successful, False otherwise
201232
202233
Raises:
203-
SkillNotFoundError: If an enabled skill folder doesn't exist or lacks SKILL.md
234+
SkillNotFoundError: If an enabled skill folder doesn't exist in any
235+
source directory or lacks SKILL.md.
204236
"""
205-
if not SKILLS_SOURCE_DIR.exists():
206-
logger.warning(f'Skills source directory not found: {SKILLS_SOURCE_DIR}')
237+
if not _SKILLS_SOURCE_DIRS:
238+
# No external source directories found. In a deployed app the skills
239+
# are bundled directly into APP_SKILLS_DIR (== _DEPLOYED_SKILLS_DIR),
240+
# so there is nothing to copy — skills are already in place.
241+
if _non_empty_dir(APP_SKILLS_DIR):
242+
logger.info(f'No external source dirs; skills already in place at {APP_SKILLS_DIR}')
243+
return True
244+
logger.warning('No skills source directories found')
207245
return False
208246

209-
# Guard against self-deletion: in deployed apps, SKILLS_SOURCE_DIR and
210-
# APP_SKILLS_DIR resolve to the same directory. Deleting APP_SKILLS_DIR
211-
# would destroy the source. Skills are already in place, so skip the copy.
212-
if SKILLS_SOURCE_DIR.resolve() == APP_SKILLS_DIR.resolve():
213-
logger.info(f'Skills source and app directory are the same ({APP_SKILLS_DIR}), skipping copy')
247+
# Guard against self-deletion: when every source *is* APP_SKILLS_DIR we
248+
# would wipe the only copy. Skills are already in place, so skip.
249+
all_same = all(
250+
src.resolve() == APP_SKILLS_DIR.resolve() for src in _SKILLS_SOURCE_DIRS
251+
)
252+
if all_same:
253+
logger.info(f'All skills sources resolve to {APP_SKILLS_DIR}, skipping copy')
214254
return True
215255

216256
enabled_skills = _get_enabled_skills()
217257
if enabled_skills:
218258
logger.info(f'Filtering skills to: {enabled_skills}')
219259

220-
# Validate that all enabled skills exist before copying
221260
for skill_name in enabled_skills:
222-
skill_path = SKILLS_SOURCE_DIR / skill_name
223-
skill_md_path = skill_path / 'SKILL.md'
224-
225-
if not skill_path.exists():
261+
found = _find_skill_source(skill_name)
262+
if found is None:
263+
searched = ', '.join(str(d) for d in _SKILLS_SOURCE_DIRS)
226264
raise SkillNotFoundError(
227-
f"Skill '{skill_name}' not found. "
228-
f"Directory does not exist: {skill_path}. "
265+
f"Skill '{skill_name}' not found in any source directory. "
266+
f"Searched: {searched}. "
229267
f"Check ENABLED_SKILLS in your .env file."
230268
)
231269

232-
if not skill_md_path.exists():
233-
raise SkillNotFoundError(
234-
f"Skill '{skill_name}' is invalid. "
235-
f"Missing SKILL.md file in: {skill_path}. "
236-
f"Each skill must have a SKILL.md file."
237-
)
238-
239270
try:
240-
# Remove existing skills directory if it exists
241271
if APP_SKILLS_DIR.exists():
242272
shutil.rmtree(APP_SKILLS_DIR)
243273

244-
# Copy skill directories (filtered by ENABLED_SKILLS if set)
245274
APP_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
246275

247-
copied_count = 0
248-
for item in SKILLS_SOURCE_DIR.iterdir():
249-
if item.is_dir() and (item / 'SKILL.md').exists():
250-
# Skip if not in enabled list (when list is specified)
276+
copied: set[str] = set()
277+
for src_dir in _SKILLS_SOURCE_DIRS:
278+
if src_dir.resolve() == APP_SKILLS_DIR.resolve():
279+
continue
280+
for item in src_dir.iterdir():
281+
if not item.is_dir() or not (item / 'SKILL.md').exists():
282+
continue
283+
if item.name in copied:
284+
continue
251285
if enabled_skills and item.name not in enabled_skills:
252286
logger.debug(f'Skipping skill (not enabled): {item.name}')
253287
continue
254288

255289
dest = APP_SKILLS_DIR / item.name
256290
shutil.copytree(item, dest)
257-
copied_count += 1
291+
copied.add(item.name)
258292
logger.debug(f'Copied skill: {item.name}')
259293

260-
logger.info(f'Copied {copied_count} skills to {APP_SKILLS_DIR}')
294+
logger.info(f'Copied {len(copied)} skills to {APP_SKILLS_DIR}')
261295
return True
262296

263297
except SkillNotFoundError:
264-
raise # Re-raise validation errors
298+
raise
265299
except Exception as e:
266300
logger.error(f'Failed to copy skills: {e}')
267301
return False

0 commit comments

Comments
 (0)