Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/user_guide/en/nodes/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The Agent node is the most fundamental node type in the DevAll platform, used to
| `tooling` | object | No | - | Tool calling configuration, see [Tooling Module](../modules/tooling/README.md) |
| `thinking` | object | No | - | Chain-of-thought configuration, e.g., chain-of-thought, reflection |
| `memories` | list | No | `[]` | Memory binding configuration, see [Memory Module](../modules/memory.md) |
| `skills` | object | No | - | Agent Skills discovery and built-in skill activation/file-read tools |
| `retry` | object | No | - | Automatic retry strategy configuration |

### Retry Strategy Configuration (retry)
Expand All @@ -27,6 +28,22 @@ The Agent node is the most fundamental node type in the DevAll platform, used to
| `max_wait_seconds` | float | `6.0` | Maximum backoff wait time |
| `retry_on_status_codes` | list[int] | `[408,409,425,429,500,502,503,504]` | HTTP status codes that trigger retry |

### Agent Skills Configuration (skills)

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | bool | `false` | Enable Agent Skills discovery for this node |
| `allow` | list[object] | `[]` | Optional allowlist of skills from the project-level `skills/` directory; each entry uses `name` |

### Agent Skills Notes

- Skills are discovered from the fixed project-level `skills/` directory.
- The runtime exposes two built-in skill tools: `activate_skill` and `read_skill_file`.
- `read_skill_file` only works after the relevant skill has been activated.
- Skill `SKILL.md` frontmatter may include optional `allowed-tools` using the Agent Skills spec format, for example `allowed-tools: run_python_script execute_code`.
- If a selected skill requires tools that are not bound on the node, that skill is skipped at runtime.
- If no compatible skills remain, the agent is explicitly instructed not to claim skill usage.

## When to Use

- **Text generation**: Writing, translation, summarization, Q&A, etc.
Expand Down Expand Up @@ -145,6 +162,23 @@ nodes:
max_wait_seconds: 10.0
```

### Configuring Agent Skills

```yaml
nodes:
- id: Skilled Agent
type: agent
config:
provider: openai
name: gpt-4o
api_key: ${API_KEY}
skills:
enabled: true
allow:
- name: python-scratchpad
- name: rest-api-caller
```

## Related Documentation

- [Tooling Module Configuration](../modules/tooling/README.md)
Expand Down
34 changes: 34 additions & 0 deletions docs/user_guide/zh/nodes/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Agent 节点是 DevAll 平台中最核心的节点类型,用于调用大语言
| `tooling` | object | 否 | - | 工具调用配置,详见 [Tooling 模块](../modules/tooling/README.md) |
| `thinking` | object | 否 | - | 思维链配置,如 chain-of-thought、reflection |
| `memories` | list | 否 | `[]` | 记忆绑定配置,详见 [Memory 模块](../modules/memory.md) |
| `skills` | object | 否 | - | Agent Skills 发现配置,以及内置的技能激活/文件读取工具 |
| `retry` | object | 否 | - | 自动重试策略配置 |

### 重试策略配置 (retry)
Expand All @@ -27,6 +28,22 @@ Agent 节点是 DevAll 平台中最核心的节点类型,用于调用大语言
| `max_wait_seconds` | float | `6.0` | 最大退避等待时间 |
| `retry_on_status_codes` | list[int] | `[408,409,425,429,500,502,503,504]` | 触发重试的 HTTP 状态码 |

### Agent Skills 配置 (skills)

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `enabled` | bool | `false` | 是否为该节点启用 Agent Skills |
| `allow` | list[object] | `[]` | 可选的技能白名单,来源于项目级 `skills/` 目录;每个条目使用 `name` |

### Agent Skills 说明

- 技能统一从固定的项目级 `skills/` 目录中发现。
- 运行时会暴露两个内置技能工具:`activate_skill` 和 `read_skill_file`。
- `read_skill_file` 只有在对应技能已经激活后才可用。
- 技能 `SKILL.md` 的 frontmatter 可以包含可选的 `allowed-tools`,格式遵循 Agent Skills 规范,例如 `allowed-tools: run_python_script execute_code`。
- 如果某个已选择技能依赖的工具没有绑定到当前节点,该技能会在运行时被跳过。
- 如果最终没有任何兼容技能可用,Agent 会被明确告知不要声称自己使用了技能。

## 何时使用

- **文本生成**:写作、翻译、摘要、问答等
Expand Down Expand Up @@ -145,6 +162,23 @@ nodes:
max_wait_seconds: 10.0
```

### 配置 Agent Skills

```yaml
nodes:
- id: Skilled Agent
type: agent
config:
provider: openai
name: gpt-4o
api_key: ${API_KEY}
skills:
enabled: true
allow:
- name: python-scratchpad
- name: rest-api-caller
```

## 相关文档

- [Tooling 模块配置](../modules/tooling/README.md)
Expand Down
2 changes: 2 additions & 0 deletions entity/configs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@
from .node.node import EdgeLink, Node
from .node.passthrough import PassthroughConfig
from .node.python_runner import PythonRunnerConfig
from .node.skills import AgentSkillsConfig
from .node.thinking import ReflectionThinkingConfig, ThinkingConfig
from .node.tooling import FunctionToolConfig, McpLocalConfig, McpRemoteConfig, ToolingConfig

__all__ = [
"AgentConfig",
"AgentRetryConfig",
"AgentSkillsConfig",
"BaseConfig",
"ConfigError",
"DesignConfig",
Expand Down
2 changes: 2 additions & 0 deletions entity/configs/node/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
from .subgraph import SubgraphConfig
from .passthrough import PassthroughConfig
from .python_runner import PythonRunnerConfig
from .skills import AgentSkillsConfig
from .node import Node
from .literal import LiteralNodeConfig

__all__ = [
"AgentConfig",
"AgentRetryConfig",
"AgentSkillsConfig",
"HumanConfig",
"SubgraphConfig",
"PassthroughConfig",
Expand Down
16 changes: 16 additions & 0 deletions entity/configs/node/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
extend_path,
)
from .memory import MemoryAttachmentConfig
from .skills import AgentSkillsConfig
from .thinking import ThinkingConfig
from entity.configs.node.tooling import ToolingConfig

Expand Down Expand Up @@ -331,6 +332,7 @@ class AgentConfig(BaseConfig):
tooling: List[ToolingConfig] = field(default_factory=list)
thinking: ThinkingConfig | None = None
memories: List[MemoryAttachmentConfig] = field(default_factory=list)
skills: AgentSkillsConfig | None = None

# Runtime attributes (attached dynamically)
token_tracker: Any | None = field(default=None, init=False, repr=False)
Expand Down Expand Up @@ -389,6 +391,10 @@ def from_dict(cls, data: Mapping[str, Any], *, path: str) -> "AgentConfig":
if "retry" in mapping and mapping["retry"] is not None:
retry_cfg = AgentRetryConfig.from_dict(mapping["retry"], path=extend_path(path, "retry"))

skills_cfg = None
if "skills" in mapping and mapping["skills"] is not None:
skills_cfg = AgentSkillsConfig.from_dict(mapping["skills"], path=extend_path(path, "skills"))

return cls(
provider=provider,
base_url=base_url,
Expand All @@ -399,6 +405,7 @@ def from_dict(cls, data: Mapping[str, Any], *, path: str) -> "AgentConfig":
tooling=tooling_cfg,
thinking=thinking_cfg,
memories=memories_cfg,
skills=skills_cfg,
retry=retry_cfg,
input_mode=input_mode,
path=path,
Expand Down Expand Up @@ -492,6 +499,15 @@ def from_dict(cls, data: Mapping[str, Any], *, path: str) -> "AgentConfig":
child=MemoryAttachmentConfig,
advance=True,
),
"skills": ConfigFieldSpec(
name="skills",
display_name="Agent Skills",
type_hint="AgentSkillsConfig",
required=False,
description="Agent Skills allowlist and built-in skill activation/file-read tools.",
child=AgentSkillsConfig,
advance=True,
),
"retry": ConfigFieldSpec(
name="retry",
display_name="Retry Policy",
Expand Down
176 changes: 176 additions & 0 deletions entity/configs/node/skills.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
"""Agent skill configuration models."""

from dataclasses import dataclass, field, replace
from pathlib import Path
from typing import Any, Dict, List, Mapping

import yaml

from entity.configs.base import (
BaseConfig,
ConfigError,
ConfigFieldSpec,
EnumOption,
optional_bool,
extend_path,
require_mapping,
)


REPO_ROOT = Path(__file__).resolve().parents[3]
DEFAULT_SKILLS_ROOT = (REPO_ROOT / "skills").resolve()
def _discover_default_skills() -> List[tuple[str, str]]:
if not DEFAULT_SKILLS_ROOT.exists() or not DEFAULT_SKILLS_ROOT.is_dir():
return []

discovered: List[tuple[str, str]] = []
for candidate in sorted(DEFAULT_SKILLS_ROOT.iterdir()):
if not candidate.is_dir():
continue
skill_file = candidate / "SKILL.md"
if not skill_file.is_file():
continue
try:
frontmatter = _parse_frontmatter(skill_file)
except Exception:
continue
raw_name = frontmatter.get("name")
raw_description = frontmatter.get("description")
if not isinstance(raw_name, str) or not raw_name.strip():
continue
if not isinstance(raw_description, str) or not raw_description.strip():
continue
discovered.append((raw_name.strip(), raw_description.strip()))
return discovered


def _parse_frontmatter(skill_file: Path) -> Mapping[str, object]:
text = skill_file.read_text(encoding="utf-8")
if not text.startswith("---"):
raise ValueError("missing frontmatter")
lines = text.splitlines()
end_idx = None
for idx in range(1, len(lines)):
if lines[idx].strip() == "---":
end_idx = idx
break
if end_idx is None:
raise ValueError("missing closing delimiter")
payload = "\n".join(lines[1:end_idx])
data = yaml.safe_load(payload) or {}
if not isinstance(data, Mapping):
raise ValueError("frontmatter must be a mapping")
return data


@dataclass
class AgentSkillSelectionConfig(BaseConfig):
name: str

FIELD_SPECS = {
"name": ConfigFieldSpec(
name="name",
display_name="Skill Name",
type_hint="str",
required=True,
description="Discovered skill name from the default repo-level skills directory.",
),
}

@classmethod
def from_dict(cls, data: Mapping[str, Any], *, path: str) -> "AgentSkillSelectionConfig":
mapping = require_mapping(data, path)
name = mapping.get("name")
if not isinstance(name, str) or not name.strip():
raise ConfigError("skill name is required", extend_path(path, "name"))
return cls(name=name.strip(), path=path)

@classmethod
def field_specs(cls) -> Dict[str, ConfigFieldSpec]:
specs = super().field_specs()
name_spec = specs.get("name")
if name_spec is None:
return specs

discovered = _discover_default_skills()
enum_values = [name for name, _ in discovered] or None
enum_options = [
EnumOption(value=name, label=name, description=description)
for name, description in discovered
] or None
description = name_spec.description or "Skill name"
if not discovered:
description = (
f"{description} (no skills found in {DEFAULT_SKILLS_ROOT})"
)
else:
description = (
f"{description} Picker options come from {DEFAULT_SKILLS_ROOT}."
)
specs["name"] = replace(
name_spec,
enum=enum_values,
enum_options=enum_options,
description=description,
)
return specs


@dataclass
class AgentSkillsConfig(BaseConfig):
enabled: bool = False
allow: List[str] = field(default_factory=list)

FIELD_SPECS = {
"enabled": ConfigFieldSpec(
name="enabled",
display_name="Enable Skills",
type_hint="bool",
required=False,
default=False,
description="Enable Agent Skills discovery and the built-in skill tools for this agent.",
advance=True,
),
"allow": ConfigFieldSpec(
name="allow",
display_name="Allowed Skills",
type_hint="list[AgentSkillSelectionConfig]",
required=False,
description="Optional allowlist of discovered skill names. Leave empty to expose every discovered skill.",
child=AgentSkillSelectionConfig,
advance=True,
),
}

@classmethod
def from_dict(cls, data: Mapping[str, Any], *, path: str) -> "AgentSkillsConfig":
mapping = require_mapping(data, path)
enabled = optional_bool(mapping, "enabled", path, default=False)
if enabled is None:
enabled = False

allow = cls._coerce_allow_entries(mapping.get("allow"), field_path=extend_path(path, "allow"))

return cls(enabled=enabled, allow=allow, path=path)

@staticmethod
def _coerce_allow_entries(value: Any, *, field_path: str) -> List[str]:
if value is None:
return []
if not isinstance(value, list):
raise ConfigError("expected list of skill entries", field_path)

result: List[str] = []
for idx, item in enumerate(value):
item_path = f"{field_path}[{idx}]"
if isinstance(item, str):
normalized = item.strip()
if normalized:
result.append(normalized)
continue
if isinstance(item, Mapping):
entry = AgentSkillSelectionConfig.from_dict(item, path=item_path)
result.append(entry.name)
continue
raise ConfigError("expected skill entry mapping or string", item_path)
return result
6 changes: 3 additions & 3 deletions functions/function_calling/code_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def execute_code(code: str, time_out: int = 60) -> str:
from pathlib import Path

def __write_script_file(_code: str):
_workspace = Path(os.getenv('TEMP_CODE_DIR', 'temp'))
_workspace = Path(os.getenv('TEMP_CODE_DIR', 'temp')).resolve()
_workspace.mkdir(exist_ok=True)
filename = f"{uuid.uuid4()}.py"
code_path = _workspace / filename
Expand All @@ -35,7 +35,7 @@ def __default_interpreter() -> str:
script_path = __write_script_file(code)
workspace = script_path.parent

cmd = [__default_interpreter(), str(script_path)]
cmd = [__default_interpreter(), str(script_path.resolve())]

try:
completed = subprocess.run(
Expand Down Expand Up @@ -63,4 +63,4 @@ def __default_interpreter() -> str:
except Exception:
pass

return stdout + stderr
return stdout + stderr
Loading
Loading