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]}"
|
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 ──
|
# ── Streaming Agent Loop ──
|
||||||
|
|
||||||
if action == 'send_message':
|
if action == 'send_message':
|
||||||
@ -291,18 +322,19 @@ if action == 'send_message':
|
|||||||
|
|
||||||
blocked, reason = _security_scan(message_text)
|
blocked, reason = _security_scan(message_text)
|
||||||
if blocked:
|
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()
|
uid = await get_user()
|
||||||
org_id = await get_userorgid() or '0'
|
org_id = await get_userorgid() or '0'
|
||||||
|
|
||||||
async def agent_stream():
|
async def agent_stream():
|
||||||
"""Async generator: yield progress chunks to frontend."""
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
async with DBPools().sqlorContext(dbname) as sor:
|
||||||
ctx = await _load_ctx(sor, uid)
|
ctx = await _load_ctx(sor, uid)
|
||||||
model = await _sel_model(sor, user_model_id)
|
model = await _sel_model(sor, user_model_id)
|
||||||
if not model:
|
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
|
return
|
||||||
|
|
||||||
repos_str = ', '.join([r['n'] for r in ctx['repos']]) or '无'
|
repos_str = ', '.join([r['n'] for r in ctx['repos']]) or '无'
|
||||||
@ -322,7 +354,7 @@ if action == 'send_message':
|
|||||||
try:
|
try:
|
||||||
raw = await _call_llm(model, msgs, 0.4)
|
raw = await _call_llm(model, msgs, 0.4)
|
||||||
except Exception as e:
|
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
|
return
|
||||||
|
|
||||||
act = _parse(raw)
|
act = _parse(raw)
|
||||||
@ -333,13 +365,21 @@ if action == 'send_message':
|
|||||||
if act.get('action') == 'tool_call':
|
if act.get('action') == 'tool_call':
|
||||||
tool = act.get('tool','')
|
tool = act.get('tool','')
|
||||||
tparams = act.get('params',{})
|
tparams = act.get('params',{})
|
||||||
# Stream: 告知前端正在做什么
|
tool_label = _tool_label(tool)
|
||||||
yield json.dumps({"type":"progress","tool":tool,"params":tparams}, ensure_ascii=False) + '\n'
|
|
||||||
|
# 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)
|
result = await _exec(sor, tool, tparams, ctx, uid, org_id)
|
||||||
|
ok = not result.startswith("错误") and not result.startswith("失败")
|
||||||
|
|
||||||
# Stream: 告知前端执行结果
|
# Widget: 工具结果
|
||||||
yield json.dumps({"type":"tool_result","tool":tool,"result":result[:500]}, ensure_ascii=False) + '\n'
|
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":"assistant","content":raw})
|
||||||
msgs.append({"role":"user","content":f"工具 {tool} 执行结果:\n{result}"})
|
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'})
|
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')
|
return await stream_response(request, agent_stream(), 'application/x-ndjson')
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user