Files
2026-08-21 16:00:13 +08:00

181 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Skill 能力包服务(CRUD + 关键词触发匹配)。
CRUD 记审计;``match_active_skills`` 加载 enabled skills 调 matcher,返回 Skill ORM
列表(供 send_message 取 prompt_fragment 注入 contract)。
"""
from app.core.errors import AppError
from app.db.session import AsyncSessionLocal
from app.models.skills import Skill
from app.modules.skills.matcher import match_skills
from app.repositories import skills as skills_repo
from app.repositories.json_utils import dumps, loads_list
from app.schemas.skills import SkillResponse, ToolCapability
from app.services.audit import record_audit_event
async def create_skill(
*,
name: str,
description: str = "",
trigger: str = "",
prompt_fragment: str = "",
tool_bindings: list[str] | None = None,
risk_level: str = "L1",
status: str = "enabled",
) -> SkillResponse:
async with AsyncSessionLocal() as session:
existing = await skills_repo.get_skill_by_name(session, name)
if existing is not None:
raise AppError("conflict", f"Skill 名「{name}」已存在。")
skill = Skill(
name=name,
description=description,
trigger=trigger,
prompt_fragment=prompt_fragment,
tool_bindings_json=dumps(tool_bindings or []),
risk_level=risk_level,
status=status,
)
await skills_repo.add_skill(session, skill)
await record_audit_event(
session,
action="skill.created",
actor="user",
input_summary=name[:200],
resource_type="skill",
resource_id=skill.id,
)
await session.commit()
await session.refresh(skill)
return _to_response(skill)
async def get_skill(skill_id: str) -> SkillResponse | None:
async with AsyncSessionLocal() as session:
skill = await skills_repo.get_skill(session, skill_id)
return _to_response(skill) if skill else None
async def list_skills(*, status: str | None = None) -> list[SkillResponse]:
async with AsyncSessionLocal() as session:
skills = await skills_repo.list_skills(session, status=status)
return [_to_response(s) for s in skills]
async def list_all_capabilities(runtime) -> list[ToolCapability]:
"""统一列举 builtin + mcp(runtime.list_specs)+ skill(DB),含来源标记。"""
capabilities: list[ToolCapability] = []
if runtime is not None:
for spec in runtime.list_specs():
source = "mcp" if spec.name.startswith("mcp:") else "builtin"
capabilities.append(
ToolCapability(
name=spec.name,
description=spec.description,
source=source,
risk_level=spec.risk_level,
status="enabled",
)
)
for s in await list_skills(status="enabled"):
capabilities.append(
ToolCapability(
name=s.name,
description=s.description,
source="skill",
risk_level=s.risk_level,
status=s.status,
)
)
return capabilities
async def update_skill(
skill_id: str,
*,
description: str | None = None,
trigger: str | None = None,
prompt_fragment: str | None = None,
tool_bindings: list[str] | None = None,
risk_level: str | None = None,
status: str | None = None,
) -> SkillResponse:
async with AsyncSessionLocal() as session:
skill = await skills_repo.get_skill(session, skill_id)
if skill is None:
raise AppError("not_found", "Skill 不存在。")
if description is not None:
skill.description = description
if trigger is not None:
skill.trigger = trigger
if prompt_fragment is not None:
skill.prompt_fragment = prompt_fragment
if tool_bindings is not None:
skill.tool_bindings_json = dumps(tool_bindings)
if risk_level is not None:
skill.risk_level = risk_level
if status is not None:
skill.status = status
await record_audit_event(
session,
action="skill.updated",
actor="user",
input_summary=skill.name[:200],
resource_type="skill",
resource_id=skill.id,
)
await session.commit()
await session.refresh(skill)
return _to_response(skill)
async def delete_skill(skill_id: str) -> None:
async with AsyncSessionLocal() as session:
skill = await skills_repo.get_skill(session, skill_id)
if skill is None:
raise AppError("not_found", "Skill 不存在。")
name = skill.name
await session.delete(skill)
await record_audit_event(
session,
action="skill.deleted",
actor="user",
input_summary=name[:200],
resource_type="skill",
resource_id=skill_id,
)
await session.commit()
async def match_active_skills(context_text: str) -> list[Skill]:
"""加载 enabled skills,关键词匹配;返回命中的 Skill ORM(供注入 prompt_fragment)。"""
async with AsyncSessionLocal() as session:
skills = await skills_repo.list_skills(session, status="enabled")
return match_skills(context_text, skills)
async def get_enabled_skills_by_names(names: tuple[str, ...]) -> list[Skill]:
"""按名称返回 enabled Skills,供 Agent 显式引用。"""
wanted = set(names)
if not wanted:
return []
async with AsyncSessionLocal() as session:
skills = await skills_repo.list_skills(session, status="enabled")
return [skill for skill in skills if skill.name in wanted]
def _to_response(skill: Skill) -> SkillResponse:
return SkillResponse(
id=skill.id,
name=skill.name,
description=skill.description,
trigger=skill.trigger,
prompt_fragment=skill.prompt_fragment,
tool_bindings=loads_list(skill.tool_bindings_json),
risk_level=skill.risk_level,
status=skill.status,
created_at=skill.created_at,
updated_at=skill.updated_at,
)