diff --git a/wwwroot/agent/index.ui b/wwwroot/agent/index.ui new file mode 100644 index 0000000..07aa34e --- /dev/null +++ b/wwwroot/agent/index.ui @@ -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": "输入你的需求..." + } + } + ] +} diff --git a/wwwroot/api/agent_chat.dspy b/wwwroot/api/agent_chat.dspy new file mode 100644 index 0000000..cbd5a86 --- /dev/null +++ b/wwwroot/api/agent_chat.dspy @@ -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']*>(.*?)', 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) diff --git a/wwwroot/api/agent_model_options.dspy b/wwwroot/api/agent_model_options.dspy new file mode 100644 index 0000000..8872f9d --- /dev/null +++ b/wwwroot/api/agent_model_options.dspy @@ -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 diff --git a/wwwroot/api/skill_pack.dspy b/wwwroot/api/skill_pack.dspy new file mode 100644 index 0000000..f67f57a --- /dev/null +++ b/wwwroot/api/skill_pack.dspy @@ -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)