Skip to content

Commit 1b3b4a3

Browse files
committed
feat(tool): 添加Skill工具下载功能
此提交添加了对SKILL类型工具的支持,包括获取下载URL和异步下载解压的功能。同时更新了相关单元测试。 Co-developed-by: Aone Copilot <noreply@alibaba-inc.com> Signed-off-by: Sodawyx <sodawyx@126.com>
1 parent 0c629bf commit 1b3b4a3

File tree

4 files changed

+505
-0
lines changed

4 files changed

+505
-0
lines changed

agentrun/tool/__tool_async_template.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44
Provides object-oriented wrapper and complete lifecycle management for tool resources.
55
"""
66

7+
import io
8+
import os
9+
import shutil
710
from typing import Any, Dict, List, Optional
11+
import zipfile
812

13+
import httpx
914
import pydash
1015

1116
from agentrun.utils.config import Config
@@ -309,3 +314,83 @@ async def call_tool_async(
309314
return result
310315

311316
raise ValueError(f"Unsupported tool type: {self.tool_type}")
317+
318+
def _get_skill_download_url(
319+
self, config: Optional[Config] = None
320+
) -> Optional[str]:
321+
"""获取 Skill 工具的下载 URL / Get download URL for Skill tools
322+
323+
根据 data_endpoint 和 tool_name 构造下载地址。
324+
Constructs download URL from data_endpoint and tool_name.
325+
326+
Returns:
327+
Optional[str]: 下载 URL / Download URL
328+
"""
329+
effective_name = self.tool_name or self.name
330+
data_endpoint = self.data_endpoint
331+
if not data_endpoint:
332+
cfg = Config.with_configs(config)
333+
data_endpoint = cfg._data_endpoint
334+
if not data_endpoint or not effective_name:
335+
return None
336+
return f"{data_endpoint}/tools/{effective_name}/download"
337+
338+
async def download_skill_async(
339+
self,
340+
target_dir: str = ".skills",
341+
config: Optional[Config] = None,
342+
) -> str:
343+
"""异步下载 Skill 包并解压到本地目录 / Download skill package and extract to local directory asynchronously
344+
345+
从数据链路下载 skill 的 zip 包,并解压到 {target_dir}/{tool_name}/ 目录下。
346+
Downloads skill zip package from data endpoint and extracts to {target_dir}/{tool_name}/ directory.
347+
348+
Args:
349+
target_dir: 目标根目录,默认为 ".skills" / Target root directory, defaults to ".skills"
350+
config: 配置对象,可选 / Configuration object, optional
351+
352+
Returns:
353+
str: 解压后的 skill 目录路径 / Extracted skill directory path
354+
355+
Raises:
356+
ValueError: 工具类型不是 SKILL 或缺少必要信息 / Tool type is not SKILL or missing required info
357+
httpx.HTTPStatusError: 下载失败 / Download failed
358+
"""
359+
tool_type = self._get_tool_type()
360+
if tool_type != ToolType.SKILL:
361+
raise ValueError(
362+
"download_skill is only available for SKILL type tools,"
363+
f" got {self.tool_type}"
364+
)
365+
366+
download_url = self._get_skill_download_url(config)
367+
if not download_url:
368+
raise ValueError(
369+
"Cannot construct download URL: data_endpoint or tool_name"
370+
" is missing"
371+
)
372+
373+
effective_name = self.tool_name or self.name
374+
skill_dir = os.path.join(target_dir, effective_name or "unknown_skill")
375+
376+
logger.debug("downloading skill from %s to %s", download_url, skill_dir)
377+
378+
cfg = Config.with_configs(config)
379+
headers = cfg.get_headers()
380+
381+
async with httpx.AsyncClient(
382+
timeout=300, follow_redirects=True
383+
) as http_client:
384+
response = await http_client.get(download_url, headers=headers)
385+
response.raise_for_status()
386+
387+
if os.path.exists(skill_dir):
388+
shutil.rmtree(skill_dir)
389+
os.makedirs(skill_dir, exist_ok=True)
390+
391+
zip_buffer = io.BytesIO(response.content)
392+
with zipfile.ZipFile(zip_buffer, "r") as zip_file:
393+
zip_file.extractall(skill_dir)
394+
395+
logger.info("skill downloaded and extracted to %s", skill_dir)
396+
return skill_dir

agentrun/tool/model.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class ToolType(str, Enum):
1717
"""MCP 协议工具 / MCP Protocol Tool"""
1818
FUNCTIONCALL = "FUNCTIONCALL"
1919
"""函数调用工具 / Function Call Tool"""
20+
SKILL = "SKILL"
21+
"""技能工具 / Skill Tool"""
2022

2123

2224
class McpConfig(BaseModel):

agentrun/tool/tool.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,13 @@
1414
Provides object-oriented wrapper and complete lifecycle management for tool resources.
1515
"""
1616

17+
import io
18+
import os
19+
import shutil
1720
from typing import Any, Dict, List, Optional
21+
import zipfile
1822

23+
import httpx
1924
import pydash
2025

2126
from agentrun.utils.config import Config
@@ -438,3 +443,141 @@ def call_tool(
438443
return result
439444

440445
raise ValueError(f"Unsupported tool type: {self.tool_type}")
446+
447+
def _get_skill_download_url(
448+
self, config: Optional[Config] = None
449+
) -> Optional[str]:
450+
"""获取 Skill 工具的下载 URL / Get download URL for Skill tools
451+
452+
根据 data_endpoint 和 tool_name 构造下载地址。
453+
Constructs download URL from data_endpoint and tool_name.
454+
455+
Returns:
456+
Optional[str]: 下载 URL / Download URL
457+
"""
458+
effective_name = self.tool_name or self.name
459+
data_endpoint = self.data_endpoint
460+
if not data_endpoint:
461+
cfg = Config.with_configs(config)
462+
data_endpoint = cfg._data_endpoint
463+
if not data_endpoint or not effective_name:
464+
return None
465+
return f"{data_endpoint}/tools/{effective_name}/download"
466+
467+
async def download_skill_async(
468+
self,
469+
target_dir: str = ".skills",
470+
config: Optional[Config] = None,
471+
) -> str:
472+
"""异步下载 Skill 包并解压到本地目录 / Download skill package and extract to local directory asynchronously
473+
474+
从数据链路下载 skill 的 zip 包,并解压到 {target_dir}/{tool_name}/ 目录下。
475+
Downloads skill zip package from data endpoint and extracts to {target_dir}/{tool_name}/ directory.
476+
477+
Args:
478+
target_dir: 目标根目录,默认为 ".skills" / Target root directory, defaults to ".skills"
479+
config: 配置对象,可选 / Configuration object, optional
480+
481+
Returns:
482+
str: 解压后的 skill 目录路径 / Extracted skill directory path
483+
484+
Raises:
485+
ValueError: 工具类型不是 SKILL 或缺少必要信息 / Tool type is not SKILL or missing required info
486+
httpx.HTTPStatusError: 下载失败 / Download failed
487+
"""
488+
tool_type = self._get_tool_type()
489+
if tool_type != ToolType.SKILL:
490+
raise ValueError(
491+
"download_skill is only available for SKILL type tools,"
492+
f" got {self.tool_type}"
493+
)
494+
495+
download_url = self._get_skill_download_url(config)
496+
if not download_url:
497+
raise ValueError(
498+
"Cannot construct download URL: data_endpoint or tool_name"
499+
" is missing"
500+
)
501+
502+
effective_name = self.tool_name or self.name
503+
skill_dir = os.path.join(target_dir, effective_name or "unknown_skill")
504+
505+
logger.debug("downloading skill from %s to %s", download_url, skill_dir)
506+
507+
cfg = Config.with_configs(config)
508+
headers = cfg.get_headers()
509+
510+
async with httpx.AsyncClient(
511+
timeout=300, follow_redirects=True
512+
) as http_client:
513+
response = await http_client.get(download_url, headers=headers)
514+
response.raise_for_status()
515+
516+
if os.path.exists(skill_dir):
517+
shutil.rmtree(skill_dir)
518+
os.makedirs(skill_dir, exist_ok=True)
519+
520+
zip_buffer = io.BytesIO(response.content)
521+
with zipfile.ZipFile(zip_buffer, "r") as zip_file:
522+
zip_file.extractall(skill_dir)
523+
524+
logger.info("skill downloaded and extracted to %s", skill_dir)
525+
return skill_dir
526+
527+
def download_skill(
528+
self,
529+
target_dir: str = ".skills",
530+
config: Optional[Config] = None,
531+
) -> str:
532+
"""同步下载 Skill 包并解压到本地目录 / Download skill package and extract to local directory synchronously
533+
534+
从数据链路下载 skill 的 zip 包,并解压到 {target_dir}/{tool_name}/ 目录下。
535+
Downloads skill zip package from data endpoint and extracts to {target_dir}/{tool_name}/ directory.
536+
537+
Args:
538+
target_dir: 目标根目录,默认为 ".skills" / Target root directory, defaults to ".skills"
539+
config: 配置对象,可选 / Configuration object, optional
540+
541+
Returns:
542+
str: 解压后的 skill 目录路径 / Extracted skill directory path
543+
544+
Raises:
545+
ValueError: 工具类型不是 SKILL 或缺少必要信息 / Tool type is not SKILL or missing required info
546+
httpx.HTTPStatusError: 下载失败 / Download failed
547+
"""
548+
tool_type = self._get_tool_type()
549+
if tool_type != ToolType.SKILL:
550+
raise ValueError(
551+
"download_skill is only available for SKILL type tools,"
552+
f" got {self.tool_type}"
553+
)
554+
555+
download_url = self._get_skill_download_url(config)
556+
if not download_url:
557+
raise ValueError(
558+
"Cannot construct download URL: data_endpoint or tool_name"
559+
" is missing"
560+
)
561+
562+
effective_name = self.tool_name or self.name
563+
skill_dir = os.path.join(target_dir, effective_name or "unknown_skill")
564+
565+
logger.debug("downloading skill from %s to %s", download_url, skill_dir)
566+
567+
cfg = Config.with_configs(config)
568+
headers = cfg.get_headers()
569+
570+
with httpx.Client(timeout=300, follow_redirects=True) as http_client:
571+
response = http_client.get(download_url, headers=headers)
572+
response.raise_for_status()
573+
574+
if os.path.exists(skill_dir):
575+
shutil.rmtree(skill_dir)
576+
os.makedirs(skill_dir, exist_ok=True)
577+
578+
zip_buffer = io.BytesIO(response.content)
579+
with zipfile.ZipFile(zip_buffer, "r") as zip_file:
580+
zip_file.extractall(skill_dir)
581+
582+
logger.info("skill downloaded and extracted to %s", skill_dir)
583+
return skill_dir

0 commit comments

Comments
 (0)