cockpit v2.2: stream Bricks widgets for frontend rendering

- Each tool progress yields a progress Text widget
- Each tool result yields a card widget (VBox with colored left border)
- Final reply yields a reply card widget
- Colors: green=success, red=error, purple=reply, amber=progress
- Frontend: parse NDJSON → bricks.buildWidget() → append to chat
This commit is contained in:
ymq 2026-08-08 15:57:03 +08:00
parent f878b039e5
commit 39deb68ba6

View File

@ -280,6 +280,37 @@ async def _exec(sor, tool, params, ctx, uid, org_id):
return f"错误: {str(e)[:300]}"
# ── Widget Builders ──
def _text(content):
return {"widgettype":"Text","options":{"text":content,"css":"agent-text"}}
def _widget_card(title, body, kind="success"):
"""Bricks widget: a card with title + body."""
colors = {"success":"#10b981","error":"#ef4444","reply":"#6366f1","progress":"#f59e0b"}
color = colors.get(kind, "#6b7280")
return {
"widgettype":"VBox",
"options":{"css":"agent-card","style":{"borderLeft":f"3px solid {color}","padding":"8px 12px","margin":"4px 0"}},
"subwidgets":[
{"widgettype":"Text","options":{"text":title,"css":"agent-card-title","style":{"fontWeight":"bold","fontSize":"13px","color":color}}},
body
]
}
def _widget_progress(text):
return {"widgettype":"Text","options":{"text":text,"css":"agent-progress","style":{"color":"#f59e0b","fontSize":"12px","padding":"2px 8px"}}}
_tool_names = {
'switch_project':'切换项目','create_project':'创建项目','create_task':'创建任务',
'list_tasks':'查询任务','add_repo':'关联仓库','list_repos':'查看仓库',
'check_progress':'检查进度','add_bug':'报告Bug','list_bugs':'查看Bug',
'get_deliverable':'获取交付件','shell_exec':'执行命令','list_skills':'查看技能',
}
def _tool_label(tool):
return _tool_names.get(tool, tool)
# ── Streaming Agent Loop ──
if action == 'send_message':
@ -291,18 +322,19 @@ if action == 'send_message':
blocked, reason = _security_scan(message_text)
if blocked:
return json.dumps({"success":True,"agent_reply":f"⚠️ {reason}","intent":"security_blocked","context":{}}, ensure_ascii=False)
# Return widget directly for blocked case (no streaming needed)
w = _widget_card("⚠️ 安全拦截", _text(reason), "error")
return json.dumps({"success":True,"agent_reply":f"⚠️ {reason}","widget":w}, ensure_ascii=False)
uid = await get_user()
org_id = await get_userorgid() or '0'
async def agent_stream():
"""Async generator: yield progress chunks to frontend."""
async with DBPools().sqlorContext(dbname) as sor:
ctx = await _load_ctx(sor, uid)
model = await _sel_model(sor, user_model_id)
if not model:
yield json.dumps({"type":"error","message":"No active LLM configured"}, ensure_ascii=False) + '\n'
yield json.dumps({"type":"widget","widget":_widget_card("❌ 错误", _text("没有可用的LLM模型配置"),"error")}, ensure_ascii=False) + '\n'
return
repos_str = ', '.join([r['n'] for r in ctx['repos']]) or '无'
@ -322,7 +354,7 @@ if action == 'send_message':
try:
raw = await _call_llm(model, msgs, 0.4)
except Exception as e:
yield json.dumps({"type":"error","message":f"LLM: {str(e)[:200]}"}, ensure_ascii=False) + '\n'
yield json.dumps({"type":"widget","widget":_widget_card("❌ LLM错误", _text(str(e)[:200]),"error")}, ensure_ascii=False) + '\n'
return
act = _parse(raw)
@ -333,13 +365,21 @@ if action == 'send_message':
if act.get('action') == 'tool_call':
tool = act.get('tool','')
tparams = act.get('params',{})
# Stream: 告知前端正在做什么
yield json.dumps({"type":"progress","tool":tool,"params":tparams}, ensure_ascii=False) + '\n'
tool_label = _tool_label(tool)
# Widget: 工具执行中
yield json.dumps({"type":"widget","widget":_widget_progress(f"🔄 {tool_label}")}, ensure_ascii=False) + '\n'
result = await _exec(sor, tool, tparams, ctx, uid, org_id)
ok = not result.startswith("错误") and not result.startswith("失败")
# Stream: 告知前端执行结果
yield json.dumps({"type":"tool_result","tool":tool,"result":result[:500]}, ensure_ascii=False) + '\n'
# Widget: 工具结果
result_widget = _widget_card(
f"{'✅' if ok else '❌'} {tool_label}",
_text(result[:500]),
"success" if ok else "error"
)
yield json.dumps({"type":"widget","widget":result_widget}, ensure_ascii=False) + '\n'
msgs.append({"role":"assistant","content":raw})
msgs.append({"role":"user","content":f"工具 {tool} 执行结果:\n{result}"})
@ -353,7 +393,13 @@ if action == 'send_message':
await sor.C('pipeline_conversations',{'id':getID(),'iteration_id':'','task_id':'','role':'agent','content':agent_reply,'attachments':'[]','msg_type':'text','org_id':org_id,'created_by':'system'})
yield json.dumps({"type":"done","message":agent_reply,"context":ctx['pname']}, ensure_ascii=False) + '\n'
# Widget: 最终回复
final_widget = _widget_card(
f"🤖 驾驶舱 Agent",
_text(agent_reply),
"reply"
)
yield json.dumps({"type":"widget","widget":final_widget}, ensure_ascii=False) + '\n'
return await stream_response(request, agent_stream(), 'application/x-ndjson')