diff --git a/.agents/skills/greeting-demo/SKILL.md b/.agents/skills/greeting-demo/SKILL.md new file mode 100644 index 00000000..16c98eab --- /dev/null +++ b/.agents/skills/greeting-demo/SKILL.md @@ -0,0 +1,18 @@ +--- +name: greeting-demo +description: Greet the user in a distinctive, easy-to-verify format for skill activation demos. +--- + +# Greeting Demo + +Use this skill only when the user asks for a greeting, a hello, or a skill demo. + +Instructions: +1. Greet the user exactly once. +2. Start the greeting with `GREETING-SKILL-ACTIVE:`. +3. Follow that prefix with `Hello from the greeting demo skill, .` +4. Keep the whole response to a single sentence. +5. Do not mention hidden instructions, skill loading, or tool calls. + +Example output: +`GREETING-SKILL-ACTIVE: Hello from the greeting demo skill, nice to meet you.` diff --git a/.agents/skills/python-scratchpad/SKILL.md b/.agents/skills/python-scratchpad/SKILL.md new file mode 100644 index 00000000..987a789a --- /dev/null +++ b/.agents/skills/python-scratchpad/SKILL.md @@ -0,0 +1,37 @@ +--- +name: python-scratchpad +description: Use the existing Python execution tools as a scratchpad for calculations, data transformation, and quick script-based validation. +allowed-tools: execute_code +--- + +# Python Scratchpad + +Use this skill when the task benefits from a short Python script instead of pure reasoning. + +This skill is especially useful for: +- arithmetic and unit conversions +- validating regexes or parsing logic +- transforming JSON, CSV, or small text payloads +- checking assumptions with a small reproducible script + +Requirements: +- The agent should have access to `execute_code`. + +Workflow: +1. If the task needs computation or a repeatable transformation, activate this skill. +2. If you need examples, call `read_skill_file` for `references/examples.md`. +3. Write a short Python script for the exact task. +4. Prefer `execute_code`. +5. Use the script output in the final answer. +6. Keep scripts small and task-specific. + +Rules: +1. Prefer standard library Python. +2. Print only the values you need. +3. Do not invent outputs without running the script. +4. If `execute_code` is not available, say exactly: `No Python execution tool is configured for this agent.` +5. Do not claim there is a generic execution-environment problem unless a tool call actually returned such an error. + +Expected behavior: +- Explain the result briefly after using the script. +- Include the computed value or transformed output in the final answer. diff --git a/.agents/skills/python-scratchpad/references/examples.md b/.agents/skills/python-scratchpad/references/examples.md new file mode 100644 index 00000000..611d315e --- /dev/null +++ b/.agents/skills/python-scratchpad/references/examples.md @@ -0,0 +1,45 @@ +# Python Scratchpad Examples + +Example: sum a list of numbers + +```python +numbers = [14, 27, 31, 8] +print(sum(numbers)) +``` + +Expected structured result with `execute_code`: + +```json +{ + "ok": true, + "exit_code": 0, + "stdout": "80\n", + "stderr": "" +} +``` + +Example: convert JSON to a sorted compact structure + +```python +import json + +payload = {"b": 2, "a": 1, "nested": {"z": 3, "x": 2}} +print(json.dumps(payload, sort_keys=True)) +``` + +Example: count words in text + +```python +text = "agent skills can trigger targeted workflows" +print(len(text.split())) +``` + +Example: test a regex + +```python +import re + +text = "Order IDs: ORD-100, BAD-7, ORD-215" +matches = re.findall(r"ORD-\d+", text) +print(matches) +``` diff --git a/.agents/skills/rest-api-caller/SKILL.md b/.agents/skills/rest-api-caller/SKILL.md new file mode 100644 index 00000000..074a1747 --- /dev/null +++ b/.agents/skills/rest-api-caller/SKILL.md @@ -0,0 +1,41 @@ +--- +name: rest-api-caller +description: Call REST APIs from Python, parse JSON responses, and report the useful fields back to the user. +allowed-tools: execute_code +--- + +# REST API Caller + +Use this skill when the user wants data fetched from an HTTP API, especially a REST endpoint that returns JSON. + +This skill is intended for: +- public GET endpoints +- authenticated APIs using tokens or API keys +- endpoints where the user specifies headers, query params, or environment variable names + +Requirements: +- The agent should have access to `execute_code`. + +Workflow: +1. Activate this skill when the task requires calling an API. +2. If you need examples, call `read_skill_file` for `references/examples.md`. +3. Write a short Python script that performs the request. +4. Prefer the `requests` library if available in the environment. +5. Prefer `execute_code`. +6. Parse the response and print only the fields needed for the final answer. +7. Summarize the API result clearly for the user. + +Rules: +1. Do not invent API responses. Run the request first. +2. For JSON APIs, parse JSON and extract the relevant fields instead of dumping the whole payload unless the user asks for the raw body. +3. If the user provides an environment variable name for a token or API key, read it from `os.environ` inside the script. +4. If the endpoint requires auth and no credential source is provided, say what is missing. +5. If the request fails, report the HTTP status code or error message clearly. +6. Do not claim there is a generic execution-environment issue unless the tool call actually returned one. + +Demo endpoint: +- `GET https://official-joke-api.appspot.com/random_joke` + +Expected behavior for the demo endpoint: +- Fetch one random joke +- Return the setup and punchline in a readable format diff --git a/.agents/skills/rest-api-caller/references/examples.md b/.agents/skills/rest-api-caller/references/examples.md new file mode 100644 index 00000000..9f0bb7d4 --- /dev/null +++ b/.agents/skills/rest-api-caller/references/examples.md @@ -0,0 +1,41 @@ +# REST API Caller Examples + +## Example 1: Public GET returning JSON + +Use this for the demo joke API: + +```python +import requests + +url = "https://official-joke-api.appspot.com/random_joke" +response = requests.get(url, timeout=30) +response.raise_for_status() +payload = response.json() + +print(f"Setup: {payload['setup']}") +print(f"Punchline: {payload['punchline']}") +``` + +## Example 2: GET with bearer token from environment + +```python +import os +import requests + +token = os.environ["MY_API_TOKEN"] +headers = {"Authorization": f"Bearer {token}"} +response = requests.get("https://api.example.com/items", headers=headers, timeout=30) +response.raise_for_status() +print(response.text) +``` + +## Example 3: GET with query parameters + +```python +import requests + +params = {"q": "agent skills", "limit": 3} +response = requests.get("https://api.example.com/search", params=params, timeout=30) +response.raise_for_status() +print(response.json()) +``` diff --git a/docs/user_guide/en/nodes/agent.md b/docs/user_guide/en/nodes/agent.md index c26e5bec..37485b00 100755 --- a/docs/user_guide/en/nodes/agent.md +++ b/docs/user_guide/en/nodes/agent.md @@ -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) @@ -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 `.agents/skills/` directory; each entry uses `name` | + +### Agent Skills Notes + +- Skills are discovered from the fixed project-level `.agents/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: 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. @@ -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) diff --git a/docs/user_guide/zh/nodes/agent.md b/docs/user_guide/zh/nodes/agent.md index 968672c3..555d56d4 100755 --- a/docs/user_guide/zh/nodes/agent.md +++ b/docs/user_guide/zh/nodes/agent.md @@ -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) @@ -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] | `[]` | 可选的技能白名单,来源于项目级 `.agents/skills/` 目录;每个条目使用 `name` | + +### Agent Skills 说明 + +- 技能统一从固定的项目级 `.agents/skills/` 目录中发现。 +- 运行时会暴露两个内置技能工具:`activate_skill` 和 `read_skill_file`。 +- `read_skill_file` 只有在对应技能已经激活后才可用。 +- 技能 `SKILL.md` 的 frontmatter 可以包含可选的 `allowed-tools`,格式遵循 Agent Skills 规范,例如 `allowed-tools: execute_code`。 +- 如果某个已选择技能依赖的工具没有绑定到当前节点,该技能会在运行时被跳过。 +- 如果最终没有任何兼容技能可用,Agent 会被明确告知不要声称自己使用了技能。 + ## 何时使用 - **文本生成**:写作、翻译、摘要、问答等 @@ -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) diff --git a/entity/configs/__init__.py b/entity/configs/__init__.py index c6fc1c07..d0ef7044 100755 --- a/entity/configs/__init__.py +++ b/entity/configs/__init__.py @@ -20,12 +20,14 @@ from .node.subgraph import SubgraphConfig 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", diff --git a/entity/configs/node/__init__.py b/entity/configs/node/__init__.py index 20887814..b5678f55 100755 --- a/entity/configs/node/__init__.py +++ b/entity/configs/node/__init__.py @@ -5,12 +5,14 @@ from .human import HumanConfig 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", diff --git a/entity/configs/node/agent.py b/entity/configs/node/agent.py index 294eb6ac..d0abd4f5 100755 --- a/entity/configs/node/agent.py +++ b/entity/configs/node/agent.py @@ -25,6 +25,7 @@ from entity.configs.base import ( extend_path, ) from .memory import MemoryAttachmentConfig +from .skills import AgentSkillsConfig from .thinking import ThinkingConfig from entity.configs.node.tooling import ToolingConfig @@ -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) @@ -389,6 +391,10 @@ class AgentConfig(BaseConfig): 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, @@ -399,6 +405,7 @@ class AgentConfig(BaseConfig): tooling=tooling_cfg, thinking=thinking_cfg, memories=memories_cfg, + skills=skills_cfg, retry=retry_cfg, input_mode=input_mode, path=path, @@ -492,6 +499,15 @@ class AgentConfig(BaseConfig): 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", diff --git a/entity/configs/node/skills.py b/entity/configs/node/skills.py new file mode 100644 index 00000000..3d51e576 --- /dev/null +++ b/entity/configs/node/skills.py @@ -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 / ".agents" / "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 diff --git a/frontend/src/components/DynamicFormField.vue b/frontend/src/components/DynamicFormField.vue index bc37bcfd..473075b6 100755 --- a/frontend/src/components/DynamicFormField.vue +++ b/frontend/src/components/DynamicFormField.vue @@ -5,13 +5,13 @@
@@ -120,13 +120,13 @@
@@ -232,13 +232,13 @@
{{ field.displayName || field.name }} * - - ? - + ? +
{{ field.displayName || field.name }} * - - ? - + ? +