Skip to content

Commit 1000cc2

Browse files
author
alcholiclg
committed
fix downloading skills
1 parent 35c02d2 commit 1000cc2

5 files changed

Lines changed: 335 additions & 28 deletions

File tree

ms_agent/skill/catalog.py

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
# Copyright (c) ModelScope Contributors. All rights reserved.
22
import os
3+
import shutil
34
import subprocess
45
import tempfile
6+
import zipfile
57
from pathlib import Path
68
from typing import Dict, List, Optional, Set
79

10+
import requests
11+
812
from ms_agent.utils.logger import get_logger
913

1014
from .loader import SkillLoader
@@ -13,6 +17,55 @@
1317

1418
logger = get_logger()
1519

20+
MODELSCOPE_SKILL_API = (
21+
"https://www.modelscope.cn/api/v1/skills/{skill_id}/archive/zip/master")
22+
23+
24+
def _download_skill_zip(skill_id: str, local_dir: str) -> str:
25+
"""Download a skill archive from the ModelScope skill hub and extract it.
26+
27+
This is a pure-HTTP fallback that does not require ``modelscope>=1.35.2``.
28+
The directory naming follows the SDK convention: ``<element_name>``.
29+
"""
30+
url = MODELSCOPE_SKILL_API.format(skill_id=skill_id)
31+
os.makedirs(local_dir, exist_ok=True)
32+
33+
_owner, name = skill_id.split("/", 1)
34+
skill_dir = os.path.join(local_dir, name)
35+
36+
resp = requests.get(url, stream=True, timeout=120)
37+
resp.raise_for_status()
38+
39+
zip_path = os.path.join(local_dir, f"{name}.zip")
40+
try:
41+
with open(zip_path, "wb") as fh:
42+
for chunk in resp.iter_content(chunk_size=8192):
43+
if chunk:
44+
fh.write(chunk)
45+
46+
if os.path.exists(skill_dir):
47+
shutil.rmtree(skill_dir)
48+
os.makedirs(skill_dir, exist_ok=True)
49+
50+
with zipfile.ZipFile(zip_path, "r") as zf:
51+
zf.extractall(skill_dir)
52+
53+
entries = os.listdir(skill_dir)
54+
if len(entries) == 1:
55+
nested = os.path.join(skill_dir, entries[0])
56+
if os.path.isdir(nested):
57+
for item in os.listdir(nested):
58+
shutil.move(
59+
os.path.join(nested, item),
60+
os.path.join(skill_dir, item))
61+
os.rmdir(nested)
62+
finally:
63+
if os.path.exists(zip_path):
64+
os.remove(zip_path)
65+
66+
logger.info(f"Skill {skill_id} downloaded to {skill_dir}")
67+
return skill_dir
68+
1669
BUILTIN_SKILLS_DIR = Path(__file__).parent.parent / "skills"
1770
if not BUILTIN_SKILLS_DIR.exists():
1871
_repo_root = Path(__file__).parent.parent.parent
@@ -129,10 +182,16 @@ def _materialize_and_load(
129182

130183
def _load_from_modelscope(
131184
self, source: SkillSource) -> Dict[str, SkillSchema]:
132-
from modelscope import snapshot_download
133-
local_path = snapshot_download(
134-
repo_id=source.repo_id,
135-
revision=source.revision or "master")
185+
try:
186+
from modelscope.hub.api import HubApi
187+
api = HubApi()
188+
local_dir = str(USER_SKILLS_DIR / "installed")
189+
local_path = api.download_skill(
190+
skill_id=source.repo_id, local_dir=local_dir)
191+
except (ImportError, AttributeError):
192+
local_path = _download_skill_zip(
193+
source.repo_id,
194+
str(USER_SKILLS_DIR / "installed"))
136195
if source.subdir:
137196
local_path = str(Path(local_path) / source.subdir)
138197
return self._loader.load_skills(local_path)

ms_agent/skill/loader.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,7 @@ def load_skills(
4141
logger.warning('No skills provided to load.')
4242
return all_skills
4343

44-
def is_skill_id(s: str) -> bool:
45-
return '/' in s and len(s.split('/')) == 2 and all(
46-
s.split('/')) and not os.path.exists(s)
47-
4844
if isinstance(skills, str):
49-
# Could be a single skill path, root path of skills, or skill ID on ModelScope hub
5045
skill_list = [skills]
5146
elif all(isinstance(s, str) for s in skills) or all(
5247
isinstance(s, SkillSchema) for s in skills):
@@ -55,12 +50,6 @@ def is_skill_id(s: str) -> bool:
5550
raise ValueError('Invalid skills input type.')
5651

5752
for skill in skill_list:
58-
59-
if is_skill_id(skill):
60-
from modelscope import snapshot_download
61-
skill_path: str = snapshot_download(repo_id=skill)
62-
skill = skill_path
63-
6453
if isinstance(skill, SkillSchema):
6554
skill_key = self._get_skill_key(skill=skill)
6655
all_skills[skill_key] = skill

ms_agent/skill/sources.py

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,40 @@ class SkillSource:
2626

2727
_MODELSCOPE_URI_RE = re.compile(
2828
r'^modelscope://(?P<repo>[^@#]+)(?:@(?P<rev>[^#]+))?(?:#(?P<sub>.+))?$')
29+
30+
_MODELSCOPE_SKILL_URL_RE = re.compile(
31+
r'^https?://(?:www\.)?modelscope\.(?:cn|ai)/skills/'
32+
r'(?P<repo>[^/]+/[^/]+)(?:/.*)?$')
33+
2934
_OWNER_REPO_RE = re.compile(r'^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$')
3035

36+
_AT_PREFIX_RE = re.compile(
37+
r'^@(?P<repo>[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)$')
38+
39+
40+
def _looks_like_path(raw: str) -> bool:
41+
"""Return True when *raw* is clearly meant to be a local filesystem path
42+
rather than a hub identifier (e.g. starts with ``/``, ``./``, ``~``, or
43+
contains path separators that don't match the ``owner/repo`` pattern).
44+
"""
45+
return raw.startswith(('/', './', '../', '~'))
46+
3147

3248
def parse_skill_source(raw: str) -> SkillSource:
3349
"""Parse a raw string into a SkillSource.
3450
35-
Supported formats:
36-
- /abs/path/to/skills -> LOCAL_DIR
37-
- ./relative/path -> LOCAL_DIR
38-
- modelscope://owner/repo@rev -> MODELSCOPE
39-
- https://... or git://... -> GIT
40-
- owner/repo -> MODELSCOPE (when path does not exist)
51+
Supported formats (checked in order):
52+
- /abs/path or ./rel/path or ~/path -> LOCAL_DIR
53+
- modelscope://owner/repo[@rev][#subdir] -> MODELSCOPE
54+
- https://modelscope.cn/skills/owner/repo -> MODELSCOPE
55+
- @owner/repo (CLI shorthand) -> MODELSCOPE
56+
- https://... or git://... -> GIT
57+
- owner/repo (when path does not exist) -> MODELSCOPE
58+
- anything else -> LOCAL_DIR
4159
"""
42-
if os.path.exists(raw):
43-
return SkillSource(type=SkillSourceType.LOCAL_DIR, path=raw)
60+
if _looks_like_path(raw):
61+
resolved = str(Path(raw).expanduser().resolve())
62+
return SkillSource(type=SkillSourceType.LOCAL_DIR, path=resolved)
4463

4564
m = _MODELSCOPE_URI_RE.match(raw)
4665
if m:
@@ -51,10 +70,25 @@ def parse_skill_source(raw: str) -> SkillSource:
5170
subdir=m.group('sub'),
5271
)
5372

73+
m = _MODELSCOPE_SKILL_URL_RE.match(raw)
74+
if m:
75+
return SkillSource(
76+
type=SkillSourceType.MODELSCOPE,
77+
repo_id=m.group('repo'),
78+
)
79+
80+
m = _AT_PREFIX_RE.match(raw)
81+
if m:
82+
return SkillSource(
83+
type=SkillSourceType.MODELSCOPE,
84+
repo_id=m.group('repo'),
85+
)
86+
5487
if raw.startswith(('https://', 'http://', 'git://')):
5588
return SkillSource(type=SkillSourceType.GIT, url=raw)
5689

57-
if _OWNER_REPO_RE.match(raw):
90+
if _OWNER_REPO_RE.match(raw) and not os.path.exists(raw):
5891
return SkillSource(type=SkillSourceType.MODELSCOPE, repo_id=raw)
5992

60-
return SkillSource(type=SkillSourceType.LOCAL_DIR, path=raw)
93+
resolved = str(Path(raw).resolve()) if not os.path.isabs(raw) else raw
94+
return SkillSource(type=SkillSourceType.LOCAL_DIR, path=resolved)

requirements/framework.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ json5
66
markdown
77
matplotlib
88
mcp
9-
modelscope
9+
modelscope>=1.35.2
1010
moviepy
1111
numpy
1212
omegaconf

0 commit comments

Comments
 (0)