# cockpit_chat_v2.dspy - SDLC Agent v2 (AgentExecutor 驱动) # 替代 cockpit_chat.dspy,使用 pipeline-core/pipeline-service v2 架构 import aiohttp 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-sdlc') # ── Widget helpers ── def _w_text(t, css='agent-text'): return {"widgettype":"Text","options":{"text":t,"halign":"left","css":css}} def _w_progress(t): return {"widgettype":"Text","options":{"text":t,"halign":"left","style":{"color":"#f0a040","fontStyle":"italic"}}} def _w_md(t): return {"widgettype":"MdWidget","options":{"text":t}} def _w_card(title, body, kind="info"): colors = {"success":"#4caf50","error":"#f44336","info":"#2196f3","warn":"#ff9800"} color = colors.get(kind, colors["info"]) title_w = {"widgettype":"Text","options":{"text":title,"halign":"left","style":{"fontWeight":"bold","color":color,"marginBottom":"4px"}}} body_w = body if isinstance(body, dict) else _w_text(str(body)) return {"widgettype":"VBox","subwidgets":[title_w, body_w], "options":{"css":"agent-card","style":{"borderLeft":"3px solid "+color,"paddingLeft":"8px","marginBottom":"8px"}}} if action == 'send_message': # 从 params 中提取 prompt prompt = (params_kw or {}).get('prompt', '') or msg prompt = prompt.strip() # ── 意图识别:停止类指令 ── 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(_w_md("已停止当前任务。"), 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' # 测试兼容 # ── 加载上下文 ── ctx = {'pid': '', 'name': ''} 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}) if recs: ctx['pid'] = getattr(recs[0], 'current_project_id', '') or '' if ctx['pid']: proj_recs = await sor.sqlExe( "SELECT name FROM sd_projects WHERE id=${p}$", {"p": ctx['pid']}) if proj_recs: ctx['name'] = getattr(proj_recs[0], 'name', '') # ── 加载 v2 引擎 ── from pipeline_core.agent_config import load_agent_config from pipeline_service.agent_loop_v2 import AgentExecutor config = await load_agent_config(project_id=ctx['pid']) executor = AgentExecutor( config=config, project_id=ctx['pid'], user_id=uid, ) async def agent_stream(): # 项目上下文 if ctx['name']: d = json.dumps(_w_progress("项目: " + ctx['name']), ensure_ascii=False) + '\n' yield d # 运行 AgentExecutor async for chunk in executor.run(prompt): data = json.loads(chunk) t = data.get('type', '') if t == 'progress': d = json.dumps(_w_progress(data.get('message', '')), ensure_ascii=False) + '\n' elif t == 'auto_tool': d = json.dumps(_w_progress("🔄 " + data.get('message', '')), ensure_ascii=False) + '\n' elif t == 'debug': d = '' # skip debug in UI continue elif t == 'tool_call': tool = data.get('tool', '') d = json.dumps(_w_card("调用: " + tool, "参数: " + json.dumps(data.get('params', {}), ensure_ascii=False), "info"), ensure_ascii=False) + '\n' elif t == 'tool_result': tool = data.get('tool', '') result = data.get('result', '') good = not result.startswith('ERROR') and not result.startswith('未知工具') # markdown 内容用 MdWidget 渲染(表格、列表等) is_md = result.startswith('|') or result.startswith('#') or result.startswith('- ') body = _w_md(result[:2000]) if is_md else result[:600] d = json.dumps(_w_card(tool + " 结果", body, "success" if good else "error"), ensure_ascii=False) + '\n' elif t == 'reply': d = json.dumps(_w_md(data.get('message', '')), ensure_ascii=False) + '\n' elif t == 'error': d = json.dumps(_w_card("错误", data.get('message', ''), "error"), ensure_ascii=False) + '\n' else: d = chunk # 透传 yield d 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)