feat: pipeline-core通用会话agent(AgentIO界面+agent_chat.dspy+模型选项)+skill_pack选装API
This commit is contained in:
parent
1abaa4ea2e
commit
bf5d12a389
26
wwwroot/agent/index.ui
Normal file
26
wwwroot/agent/index.ui
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"width": "100%", "height": "100%", "padding": "0"},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "HBox",
|
||||
"options": {"width": "100%", "alignItems": "center", "padding": "16px 24px 8px 24px", "cheight": 6, "gap": "12px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Title2", "options": {"text": "智能助手"}},
|
||||
{"widgettype": "Text", "options": {"text": "通用 AI 对话能力", "cfontsize": 0.9, "color": "#94a3b8"}},
|
||||
{"widgettype": "Filler"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "AgentIO",
|
||||
"id": "chat_io",
|
||||
"options": {
|
||||
"css": "filler",
|
||||
"url": "/pipeline-core/api/agent_chat.dspy",
|
||||
"model_dataurl": "/pipeline-core/api/agent_model_options.dspy",
|
||||
"model_cwidth": 14,
|
||||
"placeholder": "输入你的需求..."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
121
wwwroot/api/agent_chat.dspy
Normal file
121
wwwroot/api/agent_chat.dspy
Normal file
@ -0,0 +1,121 @@
|
||||
# agent_chat.dspy - 通用会话 Agent(pipeline-core,产线无关)
|
||||
# 使用 AgentExecutor v2(gateway 统一入口),是 Web 端复刻 Hermes CLI 的通用交互能力
|
||||
# 各产线/模块可在其上叠加专属能力,本 dspy 不依赖任何产线专属表
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import zipfile
|
||||
from ahserver.filestorage import FileStorage
|
||||
|
||||
|
||||
def _extract_text(path, name):
|
||||
"""提取文件文本内容(docx/txt/md等),返回文本或空字符串。二进制/无法解析返回空。"""
|
||||
ext = os.path.splitext(name)[1].lower()
|
||||
try:
|
||||
if ext in ('.txt', '.md', '.json', '.csv', '.py', '.log', '.yaml', '.yml', '.xml', '.html', '.ini'):
|
||||
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
return f.read()[:15000]
|
||||
if ext == '.docx':
|
||||
with zipfile.ZipFile(path) as z:
|
||||
xml = z.read('word/document.xml').decode('utf-8', errors='ignore')
|
||||
texts = re.findall(r'<w:t[^>]*>(.*?)</w:t>', xml)
|
||||
return '\n'.join(texts)[:15000]
|
||||
except Exception:
|
||||
pass
|
||||
return ''
|
||||
|
||||
|
||||
action = (params_kw or {}).get('action', 'send_message')
|
||||
msg = ''
|
||||
if action != 'send_message':
|
||||
p = params_kw or {}
|
||||
msg = (p.get('message_text') or '').strip()
|
||||
if not msg:
|
||||
inner = p.get('params', {})
|
||||
if isinstance(inner, dict):
|
||||
msg = (inner.get('prompt') or inner.get('message_text') or '').strip()
|
||||
if msg:
|
||||
action = 'send_message'
|
||||
|
||||
dbname = get_module_dbname('pipeline_core')
|
||||
|
||||
if action == 'send_message':
|
||||
# 从 params 中提取 prompt
|
||||
prompt = (params_kw or {}).get('prompt', '') or msg
|
||||
prompt = prompt.strip()
|
||||
|
||||
# 处理用户上传的文件(multipart file 字段 → web_path),提取文本作为中性上下文注入 prompt
|
||||
file_ctx = ''
|
||||
_fval = (params_kw or {}).get('file')
|
||||
_fpaths = _fval if isinstance(_fval, list) else ([_fval] if _fval else [])
|
||||
for _fp in _fpaths:
|
||||
try:
|
||||
_abs = FileStorage().realPath(_fp)
|
||||
_name = os.path.basename(_abs)
|
||||
_txt = _extract_text(_abs, _name)
|
||||
if _txt:
|
||||
file_ctx += f"【文件 {_name} 内容】\n{_txt}\n\n"
|
||||
else:
|
||||
file_ctx += f"【文件 {_name}】二进制文件,无法直接读取文本。\n\n"
|
||||
except Exception:
|
||||
pass
|
||||
if file_ctx:
|
||||
prompt = "用户本次上传了以下文件:\n\n" + file_ctx + "\n用户指令:" + prompt
|
||||
|
||||
# ── 意图识别:停止类指令 ──
|
||||
stop_keywords = ['停止', '取消', '停下', '终止', '停', 'stop', 'cancel', 'abort']
|
||||
if any(kw == prompt.strip().lower() or kw in prompt.strip().lower() for kw in stop_keywords):
|
||||
async def _stop_stream():
|
||||
yield json.dumps({"content": "已停止当前任务。"}, ensure_ascii=False) + '\n'
|
||||
return await stream_response(request, _stop_stream, 'text/plain; charset=utf-8')
|
||||
|
||||
uid = await get_user()
|
||||
if not uid:
|
||||
uid = 'user-01' # 测试兼容
|
||||
|
||||
# ── 走 gateway 统一入口(Web AgentIO 通道)──
|
||||
from pipeline_service.gateway import get_gateway
|
||||
gateway = get_gateway()
|
||||
|
||||
async def agent_stream():
|
||||
async for chunk in gateway.run_message("web", uid, prompt):
|
||||
data = json.loads(chunk)
|
||||
t = data.get('type', '')
|
||||
|
||||
if t == 'progress':
|
||||
yield json.dumps({"reasoning_content": data.get('message', '') + "\n"}, ensure_ascii=False) + '\n'
|
||||
elif t == 'auto_tool':
|
||||
yield json.dumps({"reasoning_content": "🔄 " + data.get('message', '') + "\n"}, ensure_ascii=False) + '\n'
|
||||
elif t == 'debug':
|
||||
continue # skip debug in UI
|
||||
elif t == 'tool_call':
|
||||
tool = data.get('tool', '')
|
||||
params = json.dumps(data.get('params', {}), ensure_ascii=False)
|
||||
yield json.dumps({"content": "**🔧 调用: " + tool + "**\n```\n" + params + "\n```\n\n"}, ensure_ascii=False) + '\n'
|
||||
elif t == 'tool_result':
|
||||
result = data.get('result', '')
|
||||
yield json.dumps({"content": result + "\n\n"}, ensure_ascii=False) + '\n'
|
||||
elif t == 'reply':
|
||||
yield json.dumps({"content": data.get('message', '')}, ensure_ascii=False) + '\n'
|
||||
elif t == 'ask_user':
|
||||
yield json.dumps({"content": "❓ " + data.get('message', '')}, ensure_ascii=False) + '\n'
|
||||
elif t == 'error':
|
||||
yield json.dumps({"error": data.get('message', '')}, ensure_ascii=False) + '\n'
|
||||
else:
|
||||
yield json.dumps({"content": chunk}, ensure_ascii=False) + '\n'
|
||||
|
||||
return await stream_response(request, agent_stream, 'text/plain; charset=utf-8')
|
||||
|
||||
elif action == 'list_messages':
|
||||
uid = await get_user()
|
||||
if not uid:
|
||||
return json.dumps({"error": "请先登录"}, ensure_ascii=False)
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
ms = await sor.sqlExe(
|
||||
"SELECT role,content,created_at FROM pipeline_conversations ORDER BY created_at ASC LIMIT 100", {})
|
||||
result = [{"role": getattr(m, 'role', ''), "content": getattr(m, 'content', '')} for m in (ms or [])]
|
||||
return json.dumps({"success": True, "messages": result}, ensure_ascii=False, default=str)
|
||||
|
||||
else:
|
||||
return json.dumps({"error": "Unknown: " + str(action)}, ensure_ascii=False)
|
||||
22
wwwroot/api/agent_model_options.dspy
Normal file
22
wwwroot/api/agent_model_options.dspy
Normal file
@ -0,0 +1,22 @@
|
||||
# agent_model_options.dspy - 返回 active 模型列表(供 AgentIO 模型选择下拉)
|
||||
# llm 表是 pipeline 库的通用配置,产线无关
|
||||
|
||||
dbname = get_module_dbname('pipeline_core')
|
||||
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT id, name, provider, model_id, capabilities FROM llm WHERE status='active' ORDER BY name",
|
||||
{}
|
||||
)
|
||||
|
||||
rows = []
|
||||
for r in recs:
|
||||
rows.append({
|
||||
'value': r.id,
|
||||
'text': f"{r.name} ({r.provider})",
|
||||
'provider': r.provider,
|
||||
'model_id': r.model_id,
|
||||
'capabilities': r.capabilities or 'text',
|
||||
})
|
||||
|
||||
return rows
|
||||
80
wwwroot/api/skill_pack.dspy
Normal file
80
wwwroot/api/skill_pack.dspy
Normal file
@ -0,0 +1,80 @@
|
||||
# skill_pack.dspy - 技能集选装/安装(机构管理员,pipeline-core 通用能力)
|
||||
# action=list: 列出可安装的技能集
|
||||
# action=installed: 查当前机构已安装的技能集
|
||||
# action=install: 安装技能集到当前机构(org scope)
|
||||
# action=uninstall: 卸载技能集
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
uid = await get_user()
|
||||
if not uid:
|
||||
return json.dumps({"success": False, "error": "请先登录"}, ensure_ascii=False)
|
||||
|
||||
action = (params_kw or {}).get('action', 'list')
|
||||
dbname = get_module_dbname('pipeline_core')
|
||||
|
||||
# 查用户 org_id + 角色(pipeline 库)
|
||||
org_id = ''
|
||||
is_admin = False
|
||||
try:
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
urecs = await sor.sqlExe("SELECT orgid FROM users WHERE id=${u}$", {"u": uid})
|
||||
if urecs:
|
||||
org_id = getattr(urecs[0], 'orgid', '') or ''
|
||||
rrecs = await sor.sqlExe(
|
||||
"SELECT r.name FROM userrole ur JOIN role r ON ur.roleid=r.id WHERE ur.userid=${u}$",
|
||||
{"u": uid})
|
||||
roles = [getattr(r, 'name', '') for r in (rrecs or [])]
|
||||
is_admin = ('superuser' in roles) or ('admin' in roles)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not org_id:
|
||||
return json.dumps({"success": False, "error": "用户未关联机构"}, ensure_ascii=False)
|
||||
|
||||
# 技能根目录(skill_loader 的 base_dir,相对进程 cwd)
|
||||
base_dir = os.environ.get("PIPELINE_SKILLS_BASE", "skills")
|
||||
|
||||
from pipeline_core.skill_pack import list_packs, install_pack, uninstall_pack, installed_packs
|
||||
|
||||
if action == 'list':
|
||||
return json.dumps({"success": True, "is_admin": is_admin, "packs": list_packs()},
|
||||
ensure_ascii=False)
|
||||
|
||||
elif action == 'installed':
|
||||
return json.dumps({"success": True, "packs": installed_packs(base_dir, org_id)},
|
||||
ensure_ascii=False)
|
||||
|
||||
elif action == 'install':
|
||||
if not is_admin:
|
||||
return json.dumps({"success": False, "error": "仅机构管理员可安装技能集"}, ensure_ascii=False)
|
||||
pack_name = (params_kw or {}).get('pack', '').strip()
|
||||
if not pack_name:
|
||||
return json.dumps({"success": False, "error": "pack 必填"}, ensure_ascii=False)
|
||||
r = install_pack(pack_name, base_dir, org_id)
|
||||
if r.get("success"):
|
||||
try:
|
||||
from pipeline_core.skill_loader import get_skill_loader
|
||||
get_skill_loader().reload()
|
||||
except Exception:
|
||||
pass
|
||||
return json.dumps(r, ensure_ascii=False)
|
||||
|
||||
elif action == 'uninstall':
|
||||
if not is_admin:
|
||||
return json.dumps({"success": False, "error": "仅机构管理员可卸载技能集"}, ensure_ascii=False)
|
||||
pack_name = (params_kw or {}).get('pack', '').strip()
|
||||
if not pack_name:
|
||||
return json.dumps({"success": False, "error": "pack 必填"}, ensure_ascii=False)
|
||||
r = uninstall_pack(pack_name, base_dir, org_id)
|
||||
if r.get("success"):
|
||||
try:
|
||||
from pipeline_core.skill_loader import get_skill_loader
|
||||
get_skill_loader().reload()
|
||||
except Exception:
|
||||
pass
|
||||
return json.dumps(r, ensure_ascii=False)
|
||||
|
||||
else:
|
||||
return json.dumps({"error": "Unknown action: " + str(action)}, ensure_ascii=False)
|
||||
Loading…
x
Reference in New Issue
Block a user