From a672dddc9a80586863de3c6a381d9a2162e66651 Mon Sep 17 00:00:00 2001 From: chiguyong Date: Tue, 23 Jun 2026 15:55:59 +0800 Subject: [PATCH] feat(skills): distinguish agent templates from business skills in UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The skills tab mixed generic execution-engine templates (react/direct/ rewoo/...) with business-domain skills (monitor/geo_optimizer/...) with no visual or data distinction. Adds a derived `category` field to the SkillInfo/SkillDetail API models and groups the frontend display. Backend: - SkillInfo/SkillDetail: add category (Literal), agent_type, execution_mode, task_mode fields - _skill_to_info: derive category from explicit _ENGINE_TEMPLATE_NAMES set (not name suffix — trend_agent/deai_agent are business skills despite the _agent suffix) - Simplify repetitive hasattr pattern with getattr Frontend: - ISkillInfo/ISkillDetail: add category + mode fields - skills store: agentTemplates/businessSkills computed getters (businessSkills is defensive: anything not explicitly engine template) - SkillsView: group into 执行引擎 / 业务技能 sections with counts - SkillCard: type badge (引擎/技能), category-based icon, mode display, dark-mode-aware accent color Tests: - test_category_derived_from_name_suffix: verifies field exposure - test_category_no_orphans: invariant — every skill has a valid category - test_trend_agent_classified_as_business_skill: regression guard for the _agent suffix misclassification bug Code review (ce-code-review): 2 P1 + 5 P2 findings applied. --- .../server/frontend/src/api/skills.ts | 11 +++ .../src/components/skills/SkillCard.vue | 81 +++++++++++++++++-- .../server/frontend/src/stores/skills.ts | 10 +++ .../server/frontend/src/views/SkillsView.vue | 79 +++++++++++++++--- .../server/routes/skill_management.py | 69 +++++++++++----- tests/unit/server/test_skill_management.py | 43 ++++++++++ 6 files changed, 255 insertions(+), 38 deletions(-) diff --git a/src/agentkit/server/frontend/src/api/skills.ts b/src/agentkit/server/frontend/src/api/skills.ts index 10889cd..6586b42 100644 --- a/src/agentkit/server/frontend/src/api/skills.ts +++ b/src/agentkit/server/frontend/src/api/skills.ts @@ -4,6 +4,8 @@ import { BaseApiClient } from './base' const API_BASE = '/api/v1/skill-management' +export type SkillCategory = 'agent_template' | 'business_skill' + export interface ISkillInfo { name: string version: string @@ -11,6 +13,11 @@ export interface ISkillInfo { capabilities: string[] dependencies: string[] status: string + /** "agent_template" = 通用执行引擎 (react/direct/rewoo/...); "business_skill" = 业务领域技能 */ + category: SkillCategory + agent_type: string + execution_mode: string + task_mode: string } export interface ISkillDetail { @@ -21,6 +28,10 @@ export interface ISkillDetail { dependencies: string[] config: Record health_status: string + category: SkillCategory + agent_type: string + execution_mode: string + task_mode: string } export interface ICapabilityInfo { diff --git a/src/agentkit/server/frontend/src/components/skills/SkillCard.vue b/src/agentkit/server/frontend/src/components/skills/SkillCard.vue index ce6a9ad..f1172c0 100644 --- a/src/agentkit/server/frontend/src/components/skills/SkillCard.vue +++ b/src/agentkit/server/frontend/src/components/skills/SkillCard.vue @@ -1,19 +1,32 @@ diff --git a/src/agentkit/server/routes/skill_management.py b/src/agentkit/server/routes/skill_management.py index 882ad76..a998c03 100644 --- a/src/agentkit/server/routes/skill_management.py +++ b/src/agentkit/server/routes/skill_management.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, Literal from fastapi import APIRouter, HTTPException, Request from pydantic import BaseModel @@ -12,6 +12,20 @@ logger = logging.getLogger(__name__) router = APIRouter(tags=["skill-management"]) +# Explicit set of generic execution-engine templates (react/direct/rewoo/...). +# Everything else is a business-domain skill. Explicit (not name-suffix based) +# because trend_agent/deai_agent are business skills despite the _agent suffix. +# ponytail: ceiling — new engine templates must be added here. Upgrade path: +# add an explicit `category` field to SkillConfig (方案 C). +_ENGINE_TEMPLATE_NAMES = frozenset({ + "react_agent", + "direct_agent", + "rewoo_agent", + "reflexion_agent", + "plan_exec_agent", + "goal_driven_agent", +}) + # --------------------------------------------------------------------------- # Request / Response models @@ -25,6 +39,10 @@ class SkillInfo(BaseModel): capabilities: list[str] dependencies: list[str] status: str + category: Literal["agent_template", "business_skill"] + agent_type: str = "" + execution_mode: str = "" + task_mode: str = "" class SkillDetail(BaseModel): @@ -35,6 +53,10 @@ class SkillDetail(BaseModel): dependencies: list[str] config: dict[str, Any] health_status: str + category: Literal["agent_template", "business_skill"] + agent_type: str = "" + execution_mode: str = "" + task_mode: str = "" class CapabilityInfo(BaseModel): @@ -50,29 +72,30 @@ class CapabilityInfo(BaseModel): def _skill_to_info(skill: Any) -> dict[str, Any]: """Convert a Skill object to a dict suitable for API responses.""" - capabilities = [] - if hasattr(skill, "config") and hasattr(skill.config, "capabilities"): - caps = skill.config.capabilities - if isinstance(caps, list): - capabilities = [c.tag if hasattr(c, "tag") else str(c) for c in caps] - elif isinstance(caps, dict): - capabilities = list(caps.keys()) + config = getattr(skill, "config", None) - dependencies = [] - if hasattr(skill, "config") and hasattr(skill.config, "dependencies"): - deps = skill.config.dependencies - if isinstance(deps, list): - dependencies = deps - elif isinstance(deps, dict): - dependencies = list(deps.keys()) + capabilities: list[str] = [] + caps = getattr(config, "capabilities", None) + if isinstance(caps, list): + capabilities = [c.tag if hasattr(c, "tag") else str(c) for c in caps] + elif isinstance(caps, dict): + capabilities = list(caps.keys()) - version = "" - if hasattr(skill, "config") and hasattr(skill.config, "version"): - version = skill.config.version or "" + deps = getattr(config, "dependencies", None) + if isinstance(deps, list): + dependencies: list[str] = deps + elif isinstance(deps, dict): + dependencies = list(deps.keys()) + else: + dependencies = [] - description = "" - if hasattr(skill, "config") and hasattr(skill.config, "description"): - description = skill.config.description or "" + version = getattr(config, "version", "") or "" + description = getattr(config, "description", "") or "" + agent_type = getattr(config, "agent_type", "") or "" + execution_mode = getattr(config, "execution_mode", "") or "" + task_mode = getattr(config, "task_mode", "") or "" + + category = "agent_template" if skill.name in _ENGINE_TEMPLATE_NAMES else "business_skill" return { "name": skill.name, @@ -81,6 +104,10 @@ def _skill_to_info(skill: Any) -> dict[str, Any]: "capabilities": capabilities, "dependencies": dependencies, "status": "active", + "category": category, + "agent_type": agent_type, + "execution_mode": execution_mode, + "task_mode": task_mode, } diff --git a/tests/unit/server/test_skill_management.py b/tests/unit/server/test_skill_management.py index 6ef9107..b31f176 100644 --- a/tests/unit/server/test_skill_management.py +++ b/tests/unit/server/test_skill_management.py @@ -113,6 +113,49 @@ class TestListSkills: assert "dependencies" in skill assert "status" in skill + def test_category_derived_from_name_suffix(self, client, skill_registry): + """category field distinguishes agent templates from business skills. + + *_agent -> "agent_template" + others -> "business_skill" + Also verifies agent_type/execution_mode/task_mode are exposed. + """ + _register_skill(skill_registry, "react_agent", execution_mode="react") + _register_skill(skill_registry, "geo_optimizer") + + response = client.get("/api/v1/skill-management/skills") + data = response.json() + by_name = {s["name"]: s for s in data["skills"]} + + agent = by_name["react_agent"] + assert agent["category"] == "agent_template" + assert agent["execution_mode"] == "react" + assert agent["agent_type"] == "test_type" + assert agent["task_mode"] == "llm_generate" + + biz = by_name["geo_optimizer"] + assert biz["category"] == "business_skill" + + def test_category_no_orphans(self, client, skill_registry): + """Every skill must fall into exactly one category — no orphans.""" + _register_skill(skill_registry, "react_agent") + _register_skill(skill_registry, "geo_optimizer") + _register_skill(skill_registry, "trend_agent") + + response = client.get("/api/v1/skill-management/skills") + data = response.json() + valid = {"agent_template", "business_skill"} + assert all(s["category"] in valid for s in data["skills"]) + + def test_trend_agent_classified_as_business_skill(self, client, skill_registry): + """trend_agent has _agent suffix but is a business-domain skill, not an engine.""" + _register_skill(skill_registry, "trend_agent") + + response = client.get("/api/v1/skill-management/skills") + data = response.json() + skill = next(s for s in data["skills"] if s["name"] == "trend_agent") + assert skill["category"] == "business_skill" + # --------------------------------------------------------------------------- # GET /skill-management/skills/{skill_name}