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:
parent
f878b039e5
commit
39deb68ba6
@ -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')
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user