181 lines
6.2 KiB
Python
181 lines
6.2 KiB
Python
"""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,
|
||
)
|