feat: 技能检索LLM化(get_skill_catalog+按名构建)+PipelineAbility.menus产线功能菜单+agent_menus.dspy+agent界面菜单栏

This commit is contained in:
yumoqing 2026-08-16 17:19:47 +08:00
parent 5ff1358116
commit 73c41e832f
4 changed files with 104 additions and 0 deletions

View File

@ -50,6 +50,7 @@ class PipelineAbility:
system_prompt: str = "" # 产线专属 prompt 片段(追加到通用心智后)
handlers: Dict[str, Callable] = field(default_factory=dict) # {tool_name: handler}
roles: List[RoleSpec] = field(default_factory=list) # 产线角色集
menus: List[Dict] = field(default_factory=list) # 产线功能菜单AgentIO 上方)[{"label","icon","url","type"}]
# ── 全局注册表 ──
@ -122,3 +123,9 @@ def list_roles(pipeline_id: str) -> List[RoleSpec]:
"""列出某产线的全部角色。"""
a = get_ability(pipeline_id)
return a.roles if a else []
def get_ability_menus(pipeline_id: str) -> List[Dict]:
"""取某产线的功能菜单AgentIO 上方,菜单/卡片/按钮)。"""
a = get_ability(pipeline_id)
return a.menus if a else []

View File

@ -277,6 +277,29 @@ class SkillLoader:
return merged
def get_skill_catalog(self, pipeline_id: str = "", role: str = "",
project_id: str = "", org_id: str = "",
user_id: str = "") -> List[tuple]:
"""返回可用技能目录 [(name, description)],供 LLM 语义检索(技能检索必须 LLM 做)。"""
merged = self.get_merged(pipeline_id, role, project_id, org_id, user_id)
return [(s.name, (s.description or "")[:150]) for s in sorted(
merged.values(), key=lambda s: (SCOPE_PRIORITY.get(s.scope, 9), s.name))]
def build_prompt_block_by_names(self, names: List[str],
pipeline_id: str = "", role: str = "",
project_id: str = "", org_id: str = "",
user_id: str = "") -> str:
"""按技能名构建目录层LLM 检索结果 → 目录层)。"""
merged = self.get_merged(pipeline_id, role, project_id, org_id, user_id)
skills = [merged[n] for n in names if n in merged]
if not skills:
return ""
blocks = ["## 可用技能\n"]
for s in skills:
blocks.append(f"- **[{SCOPE_TAG.get(s.scope, s.scope)}] {s.name}**: {s.description[:200]}")
blocks.append("")
return "\n".join(blocks)
def get_by_trigger(self, user_input: str, pipeline_id: str = "",
role: str = "", project_id: str = "",
org_id: str = "", user_id: str = "",

View File

@ -11,6 +11,11 @@
{"widgettype": "Filler"}
]
},
{
"widgettype": "urlwidget",
"id": "ability_menu",
"options": {"url": "{{entire_url('/pipeline_core/api/agent_menus.dspy')}}", "method": "GET"}
},
{
"widgettype": "AgentIO",
"id": "chat_io",

View File

@ -0,0 +1,69 @@
# agent_menus.dspy - 返回当前产线的功能菜单AgentIO 上方菜单/按钮)
# 由 core 通用 agent 的 index.ui 通过 urlwidget 加载,动态渲染产线固化的联机功能入口
import json as _json
uid = await get_user()
if not uid:
uid = 'user-01'
# 1. 解析当前产线:用户当前项目 → sd_projects.pipeline_id
pipeline_id = ''
try:
dbname = get_module_dbname('pipeline_core')
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.sqlExe(
"SELECT current_project_id FROM pipeline_agent_settings WHERE user_id=${u}$",
{"u": uid})
pid = getattr(recs[0], 'current_project_id', '') if recs else ''
if pid:
precs = await sor.sqlExe(
"SELECT pipeline_id FROM sd_projects WHERE id=${p}$", {"p": pid})
if precs:
pipeline_id = getattr(precs[0], 'pipeline_id', '') or ''
except Exception:
pass
if not pipeline_id:
try:
from pipeline_core import DEFAULT_ABILITY_ID
pipeline_id = DEFAULT_ABILITY_ID
except Exception:
pipeline_id = 'sdlc_general'
# 2. 读产线功能菜单
try:
from pipeline_core.ability import get_ability_menus
menus = get_ability_menus(pipeline_id)
except Exception:
menus = []
# 3. 构建菜单按钮(点击 → Popup 打开功能 .ui
buttons = []
for i, m in enumerate(menus):
label = str(m.get("label", "") or "")
url = str(m.get("url", "") or "")
if not url:
continue
script = (
"var pw=new bricks.PopupWindow({title:" + _json.dumps(label) +
",cwidth:62,cheight:32,auto_open:true});"
"pw.content_w.add_widget(new bricks.urlwidget({url:" + _json.dumps(url) + "}));"
)
buttons.append({
"widgettype": "Button",
"options": {"label": label, "css": "small", "id": "menu_btn_%d" % i},
"binds": [{
"wid": "self", "event": "click", "actiontype": "script",
"target": "self", "script": script,
}],
})
if not buttons:
return _json.dumps({"widgettype": "HBox", "options": {"cheight": 0}, "subwidgets": []})
return _json.dumps({
"widgettype": "HBox",
"options": {"width": "100%", "padding": "0 24px 8px 24px", "gap": "8px", "alignItems": "center"},
"subwidgets": buttons,
}, ensure_ascii=False)