- Parse file_paths from request, read file contents (UTF-8, max 8000 chars)
- File content appended to LLM message as 【附件文件内容】
- File widget streamed to frontend: 📎 filename (size) + hidden content preview
- Frontend script: click .file-name → toggle .file-content visibility
- File paths saved in pipeline_conversations.attachments
469 lines
24 KiB
Plaintext
469 lines
24 KiB
Plaintext
# cockpit_chat.dspy - SDLC Agent Loop v2.1 (streaming)
|
||
# POST: action=send_message, message_text, model_id
|
||
# GET: action=list_messages, iteration_id
|
||
# Streaming mode: 每个 tool call / 阶段 实时推送进度到前端
|
||
|
||
import aiohttp
|
||
import json
|
||
import os
|
||
|
||
action = (params_kw or {}).get('action', 'list_messages')
|
||
dbname = get_module_dbname('pipeline-sdlc')
|
||
|
||
# ── Persona ──
|
||
|
||
AGENT_PROMPT = """你是开发产线驾驶舱 Agent,负责软件工程全生命周期管理。
|
||
|
||
## 工具
|
||
__TOOLS__
|
||
|
||
## 规则
|
||
1. 根据用户输入自主决定调用哪些工具、顺序
|
||
2. 可以多轮分步推进,每轮一个工具调用
|
||
3. 最终用 reply 给出中文总结
|
||
4. 简洁专业,安全底线
|
||
5. 创建任务后可以等待检查进度,直到有确定结果再回复用户
|
||
|
||
## 当前环境
|
||
__ENV__"""
|
||
|
||
# ── Tools ──
|
||
|
||
TOOLS = [
|
||
{"name":"switch_project","description":"切换项目,支持部分名称匹配","params":{"project":"项目名称或关键字"}},
|
||
{"name":"create_project","description":"创建新项目","params":{"name":"项目名称","description":"项目描述(可选)"}},
|
||
{"name":"create_task","description":"提交任务到角色Agent队列","params":{"title":"标题","description":"详细描述","role":"requirement/design/develop/test/deploy,不指定自动推断"}},
|
||
{"name":"list_tasks","description":"列出项目任务","params":{"state":"状态过滤(可选)"}},
|
||
{"name":"add_repo","description":"关联Git仓库","params":{"url":"git地址","name":"仓库名(可选)"}},
|
||
{"name":"list_repos","description":"列出仓库","params":{}},
|
||
{"name":"check_progress","description":"检查指定任务或项目总体进度,包括状态变化、Agent执行情况、PM审核结果","params":{"task_id":"任务ID(可选,不传则查项目全局)"}},
|
||
{"name":"add_bug","description":"报告Bug","params":{"title":"标题","description":"描述","severity":"critical/major/minor(可选)"}},
|
||
{"name":"list_bugs","description":"列出Bug","params":{}},
|
||
{"name":"get_deliverable","description":"获取交付件内容","params":{"task_id":"任务ID"}},
|
||
{"name":"shell_exec","description":"执行shell命令(git clone等)","params":{"command":"命令","workdir":"工作目录(可选)"}},
|
||
{"name":"list_skills","description":"列出开发技能","params":{}},
|
||
]
|
||
|
||
TOOLS_TEXT = json.dumps(TOOLS, ensure_ascii=False, indent=2)
|
||
|
||
# ── Security ──
|
||
|
||
SECURITY_RULES = [
|
||
('drop table', 'drop database', 'truncate table', '删库', '清空数据库', '破坏性SQL操作'),
|
||
('rm -rf', 'mkfs', '格式化磁盘', '破坏文件系统的危险命令'),
|
||
('忽略之前的', '忽略上面所有', 'ignore previous', 'ignore all instructions', '提示注入'),
|
||
('绕过权限', '绕过鉴权', '关闭rbac', '越权操作'),
|
||
]
|
||
|
||
def _security_scan(text):
|
||
t = (text or '').lower()
|
||
for patterns in SECURITY_RULES:
|
||
reason = patterns[-1]
|
||
for p in patterns[:-1]:
|
||
if p.lower() in t: return True, reason
|
||
cred_kw = ('api_key', 'apikey', 'access_token', 'secret_key', '密钥', '数据库密码')
|
||
cred_vb = ('给我', '发我', '泄露', '输出', '打印', 'tell me', 'give me', 'show me')
|
||
if any(k in t for k in cred_kw) and any(v.lower() in t for v in cred_vb):
|
||
return True, '索取系统凭据'
|
||
return False, ''
|
||
|
||
|
||
# ── Helpers ──
|
||
|
||
def _guess_role(title):
|
||
t = (title or '').lower()
|
||
m = {'requirement':['需求分析','需求文档','需求规格','requirement','调研'],
|
||
'design':['设计','design','架构','方案','原型','ui','ux'],
|
||
'develop':['开发','编码','实现','编写','develop','code','build','重构','修复'],
|
||
'test':['测试','test','验证','检查','review','评审'],
|
||
'deploy':['部署','发布','deploy','release','上线','配置']}
|
||
for role, kws in m.items():
|
||
for kw in kws:
|
||
if kw in t: return role
|
||
return 'develop'
|
||
|
||
async def _find_project(sor, name):
|
||
if not name: return None
|
||
recs = await sor.sqlExe("SELECT id, name FROM sd_projects WHERE name=${n}$", {"n":name})
|
||
if recs: return recs[0]
|
||
allp = await sor.sqlExe("SELECT id, name FROM sd_projects ORDER BY created_at DESC",{})
|
||
kw = name.lower()
|
||
for r in (allp or []):
|
||
if kw in (getattr(r,'name','') or '').lower(): return r
|
||
return None
|
||
|
||
async def _load_ctx(sor, uid):
|
||
recs = await sor.sqlExe("SELECT current_project_id FROM pipeline_agent_settings WHERE user_id=${uid}$",{"uid":uid})
|
||
ctx = {'pid':'','pname':'','ws':'','skills_dir':'','repos':[]}
|
||
if not recs: return ctx
|
||
ctx['pid'] = getattr(recs[0],'current_project_id','') or ''
|
||
if ctx['pid']:
|
||
projs = await sor.sqlExe("SELECT name,workspace_dir,org_id FROM sd_projects WHERE id=${pid}$",{"pid":ctx['pid']})
|
||
if projs:
|
||
ctx['pname'] = getattr(projs[0],'name','')
|
||
ctx['ws'] = getattr(projs[0],'workspace_dir','') or ''
|
||
oid = getattr(projs[0],'org_id','') or '0'
|
||
orgs = await sor.sqlExe("SELECT skills_dir FROM sd_org_settings WHERE org_id=${oid}$",{"oid":oid})
|
||
if orgs: ctx['skills_dir'] = getattr(orgs[0],'skills_dir','') or ''
|
||
repos = await sor.sqlExe("SELECT repo_name,repo_url FROM sd_project_repos WHERE project_id=${pid}$",{"pid":ctx['pid']})
|
||
ctx['repos'] = [{'n':r.repo_name,'u':r.repo_url} for r in (repos or [])]
|
||
return ctx
|
||
|
||
async def _save_ctx(sor, uid, pid, iid=''):
|
||
existing = await sor.sqlExe("SELECT id FROM pipeline_agent_settings WHERE user_id=${uid}$",{"uid":uid})
|
||
if existing:
|
||
await sor.sqlExe("UPDATE pipeline_agent_settings SET current_project_id=${p}$,current_iteration_id=${i}$ WHERE user_id=${uid}$",{"p":pid or '','i':iid or '','uid':uid})
|
||
else:
|
||
await sor.C('pipeline_agent_settings',{'id':getID(),'user_id':uid,'default_llm_id':'','current_project_id':pid or '','current_iteration_id':iid or ''})
|
||
|
||
async def _sel_model(sor, preferred):
|
||
if preferred:
|
||
recs = await sor.sqlExe("SELECT id,name,provider,model_id,api_base,api_key FROM llm WHERE (id=${l}$ OR name=${l}$) AND status='active'",{"l":preferred})
|
||
if recs: return recs[0]
|
||
recs = await sor.sqlExe("SELECT id,name,provider,model_id,api_base,api_key FROM llm WHERE status='active' LIMIT 1",{})
|
||
return recs[0] if recs else None
|
||
|
||
async def _call_llm(model, msgs, temp):
|
||
headers = {"Authorization": f"Bearer {model.api_key or ''}", "Content-Type": "application/json"}
|
||
payload = {"model": model.model_id, "messages": msgs, "temperature": temp}
|
||
timeout = aiohttp.ClientTimeout(total=180)
|
||
async with aiohttp.ClientSession(timeout=timeout) as s:
|
||
async with s.post(f"{model.api_base.rstrip('/')}/chat/completions", headers=headers, json=payload) as r:
|
||
if r.status != 200:
|
||
t = await r.text()
|
||
raise ValueError(f"LLM {r.status}: {t[:300]}")
|
||
d = await r.json()
|
||
return d["choices"][0]["message"]["content"]
|
||
|
||
def _parse(raw):
|
||
raw = (raw or "").strip()
|
||
if raw.startswith("```"): raw = raw.split("\n",1)[1].rsplit("```",1)[0].strip()
|
||
try:
|
||
d = json.loads(raw)
|
||
if isinstance(d,dict) and 'action' in d: return d
|
||
except: pass
|
||
return {"action":"reply","message":raw}
|
||
|
||
|
||
# ── Tool Executor ──
|
||
|
||
async def _exec(sor, tool, params, ctx, uid, org_id):
|
||
try:
|
||
p = params or {}
|
||
if tool == 'switch_project':
|
||
proj = await _find_project(sor, p.get('project',''))
|
||
if proj:
|
||
await _save_ctx(sor, uid, proj.id)
|
||
ctx['pid'] = proj.id; ctx['pname'] = proj.name
|
||
return f"已切换到「{proj.name}」"
|
||
allp = await sor.sqlExe("SELECT name FROM sd_projects ORDER BY created_at DESC LIMIT 15",{})
|
||
names = ', '.join([getattr(r,'name','') for r in (allp or [])])
|
||
return f"未找到。可用项目: {names}"
|
||
|
||
elif tool == 'create_project':
|
||
name = p.get('name','')[:80]
|
||
if not name: return "缺少项目名称"
|
||
ex = await sor.sqlExe("SELECT id FROM sd_projects WHERE name=${n}$",{"n":name})
|
||
if ex: return f"「{name}」已存在"
|
||
pid = getID()
|
||
ws = os.path.expanduser(f'~/pipeline_ws/{name}')
|
||
os.makedirs(ws, exist_ok=True)
|
||
await sor.C('sd_projects',{'id':pid,'name':name,'description':p.get('description',''),'project_type':'software','org_id':org_id,'created_by':uid,'status':'active','workspace_dir':ws})
|
||
iid = getID()
|
||
await sor.C('sd_iterations',{'id':iid,'project_id':pid,'iteration_name':'默认迭代','iteration_type':'sprint','org_id':org_id,'created_by':uid,'status':'active'})
|
||
await _save_ctx(sor, uid, pid, iid)
|
||
ctx['pid'] = pid; ctx['pname'] = name
|
||
return f"✅ 项目「{name}」已创建(目录: {ws})"
|
||
|
||
elif tool == 'create_task':
|
||
if not ctx['pid']: return "请先切换到项目"
|
||
title = p.get('title','')[:100]
|
||
if not title: return "缺少标题"
|
||
role = p.get('role','') or _guess_role(title)
|
||
tp = {'description':p.get('description',''), 'project_id':ctx['pid']}
|
||
r = await pipeline_role_submit(ctx['pid'],'role_task',uid,title,tp,role)
|
||
rd = json.loads(r)
|
||
if rd.get('success'):
|
||
tid = rd.get('task_id','')
|
||
return f"✅ 任务已创建\n标题: {title}\n角色: {role}\nID: {tid[:12]}\nAgent将在15秒内认领执行"
|
||
return f"创建失败: {rd.get('message','')}"
|
||
|
||
elif tool == 'list_tasks':
|
||
if not ctx['pid']: return "请先切换到项目"
|
||
sf = p.get('state','')
|
||
sql = "SELECT title,state,role,created_at FROM pipeline_tasks WHERE tenant_id=${p}$"
|
||
if sf: sql += " AND state=${s}$"
|
||
sql += " ORDER BY created_at DESC LIMIT 20"
|
||
ts = await sor.sqlExe(sql, {"p":ctx['pid'],"s":sf} if sf else {"p":ctx['pid']})
|
||
if not ts: return "暂无任务"
|
||
em = {'submitted':'⏳','running':'🔄','review':'👀','approved':'✅','completed':'🏁','failed':'❌','waiting':'⏸️'}
|
||
return '\n'.join([f"{em.get(t.state,'❓')} [{t.state}][{t.role}] {t.title}" for t in ts])
|
||
|
||
elif tool == 'add_repo':
|
||
if not ctx['pid']: return "请先切换到项目"
|
||
url = p.get('url','')
|
||
if not url: return "请提供仓库地址"
|
||
name = p.get('name','') or url.rstrip('/').split('/')[-1].replace('.git','')
|
||
ex = await sor.sqlExe("SELECT id FROM sd_project_repos WHERE project_id=${p}$ AND repo_url=${u}$",{"p":ctx['pid'],"u":url})
|
||
if ex: return "仓库已关联"
|
||
await sor.C('sd_project_repos',{'id':getID(),'project_id':ctx['pid'],'repo_name':name,'repo_url':url,'default_branch':'main','local_path':'','org_id':org_id})
|
||
ctx['repos'].append({'n':name,'u':url})
|
||
return f"✅ 已关联 {name}"
|
||
|
||
elif tool == 'list_repos':
|
||
if not ctx['pid']: return "请先切换到项目"
|
||
rs = await sor.sqlExe("SELECT repo_name,repo_url FROM sd_project_repos WHERE project_id=${p}$",{"p":ctx['pid']})
|
||
if not rs: return "暂无仓库"
|
||
return '\n'.join([f"· {r.repo_name}: {r.repo_url}" for r in rs])
|
||
|
||
elif tool == 'check_progress':
|
||
if not ctx['pid']: return "请先切换到项目"
|
||
tid = p.get('task_id','')
|
||
if tid:
|
||
t = await sor.sqlExe("SELECT title,state,role FROM pipeline_tasks WHERE id=${t}$",{"t":tid})
|
||
if not t: return "任务不存在"
|
||
t = t[0]
|
||
ds = await sor.sqlExe("SELECT deliverable_type,review_status FROM pipeline_deliverables WHERE task_id=${t}$ ORDER BY created_at DESC LIMIT 1",{"t":tid})
|
||
dl = f"交付: {ds[0].deliverable_type}({ds[0].review_status})" if ds else "无交付件"
|
||
return f"[{t.state}][{t.role}] {t.title}\n{dl}"
|
||
# Global
|
||
ts = await sor.sqlExe("SELECT state,count(*) as c FROM pipeline_tasks WHERE tenant_id=${p}$ GROUP BY state",{"p":ctx['pid']})
|
||
summary = ', '.join([f"{t.state}:{t.c}" for t in (ts or [])]) or "无"
|
||
ags = await sor.sqlExe("SELECT role_name,status FROM pipeline_project_agents WHERE project_id=${p}$",{"p":ctx['pid']})
|
||
al = ', '.join([f"{a.role_name}({a.status})" for a in (ags or [])]) or "未配置"
|
||
ds = await sor.sqlExe("SELECT deliverable_type,title,review_status FROM pipeline_deliverables WHERE project_id=${p}$ ORDER BY created_at DESC LIMIT 3",{"p":ctx['pid']})
|
||
dl = '\n'.join([f" [{d.deliverable_type}] {d.title[:50]}({d.review_status})" for d in (ds or [])]) or "无"
|
||
return f"任务: {summary}\nAgent: {al}\n最近交付:\n{dl}"
|
||
|
||
elif tool == 'add_bug':
|
||
if not ctx['pid']: return "请先切换到项目"
|
||
title = p.get('title','')[:100]
|
||
if not title: return "缺少标题"
|
||
await sor.C('sd_bugs',{'id':getID(),'iteration_id':'','title':title,'description':p.get('description',''),'severity':p.get('severity','major'),'priority':'P1','status':'open','reporter_type':'human','reporter_id':uid})
|
||
return f"🐛 Bug已记录: {title}"
|
||
|
||
elif tool == 'list_bugs':
|
||
if not ctx['pid']: return "请先切换到项目"
|
||
bs = await sor.sqlExe("SELECT title,severity,status FROM sd_bugs ORDER BY created_at DESC LIMIT 20",{})
|
||
if not bs: return "暂无Bug"
|
||
return '\n'.join([f"· [{b.severity}] {b.title} ({b.status})" for b in bs])
|
||
|
||
elif tool == 'get_deliverable':
|
||
tid = p.get('task_id','')
|
||
if not tid: return "请提供任务ID"
|
||
ds = await sor.sqlExe("SELECT deliverable_type,title,content,file_path FROM pipeline_deliverables WHERE task_id=${t}$ ORDER BY created_at DESC LIMIT 1",{"t":tid})
|
||
if not ds: return "无交付件"
|
||
d = ds[0]
|
||
return f"[{d.deliverable_type}] {d.title}\n文件: {d.file_path or '无'}\n内容:\n{(d.content or '')[:3000]}"
|
||
|
||
elif tool == 'shell_exec':
|
||
cmd = p.get('command','')
|
||
wd = p.get('workdir','') or ctx.get('ws','')
|
||
if not cmd: return "请提供命令"
|
||
r = await shell_exec(cmd, workdir=wd, timeout=120)
|
||
return f"rc={r['rc']}\n{r['stdout'][:2000]}" if r['rc']==0 else f"失败 rc={r['rc']}\n{r['stderr'][:1000]}"
|
||
|
||
elif tool == 'list_skills':
|
||
sd = ctx.get('skills_dir','')
|
||
if not sd or not os.path.isdir(sd): return "Skills目录未配置"
|
||
found = {}
|
||
for rd in sorted(os.listdir(sd)):
|
||
rp = os.path.join(sd, rd)
|
||
if os.path.isdir(rp):
|
||
ss = [d for d in os.listdir(rp) if os.path.isdir(os.path.join(rp,d)) and os.path.isfile(os.path.join(rp,d,'SKILL.md'))]
|
||
if ss: found[rd] = ss
|
||
if not found: return "无技能"
|
||
return '\n'.join([f"[{r}] {', '.join(sorted(n))}" for r,n in sorted(found.items())])
|
||
|
||
return f"未知工具: {tool}"
|
||
except Exception as e:
|
||
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)
|
||
|
||
def _widget_file(filename, content_preview, file_size=0):
|
||
"""Bricks widget: file attachment with expandable content preview."""
|
||
size_str = f" ({file_size} bytes)" if file_size else ""
|
||
return {
|
||
"widgettype": "VBox",
|
||
"options": {
|
||
"css": "agent-file",
|
||
"style": {"border": "1px solid #e2e8f0", "borderRadius": "8px", "padding": "8px 12px", "margin": "4px 0"}
|
||
},
|
||
"subwidgets": [
|
||
{"widgettype": "HBox", "subwidgets": [
|
||
{"widgettype": "Text", "options": {"text": f"📎 {filename}{size_str}", "css": "file-name",
|
||
"style": {"color": "#2563eb", "cursor": "pointer", "fontSize": "13px", "fontWeight": "bold"}}}
|
||
]},
|
||
{"widgettype": "Text", "options": {"text": content_preview or "(空文件)", "css": "file-content",
|
||
"style": {"display": "none", "whiteSpace": "pre-wrap", "fontSize": "12px", "color": "#475569",
|
||
"background": "#f8fafc", "borderRadius": "4px", "padding": "8px", "marginTop": "6px",
|
||
"maxHeight": "300px", "overflowY": "auto"}}}
|
||
]
|
||
}
|
||
|
||
|
||
# ── Streaming Agent Loop ──
|
||
|
||
if action == 'send_message':
|
||
message_text = (params_kw or {}).get('message_text', '').strip()
|
||
user_model_id = (params_kw or {}).get('model_id', '')
|
||
file_paths_raw = (params_kw or {}).get('file_paths', '[]')
|
||
file_paths = []
|
||
try:
|
||
file_paths = json.loads(file_paths_raw) if isinstance(file_paths_raw, str) else (file_paths_raw or [])
|
||
except (json.JSONDecodeError, TypeError):
|
||
file_paths = []
|
||
|
||
if not message_text:
|
||
return json.dumps({"error": "message_text is required"}, ensure_ascii=False)
|
||
|
||
blocked, reason = _security_scan(message_text)
|
||
if blocked:
|
||
# 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 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":"widget","widget":_widget_card("❌ 错误", _text("没有可用的LLM模型配置"),"error")}, ensure_ascii=False) + '\n'
|
||
return
|
||
|
||
repos_str = ', '.join([r['n'] for r in ctx['repos']]) or '无'
|
||
env_text = f"项目: {ctx['pname'] or '未选择'}\n工作目录: {ctx['ws'] or '未配置'}\n仓库: {repos_str}\nSkills: {ctx['skills_dir'] or '未配置'}"
|
||
|
||
history = await sor.sqlExe("SELECT role,content FROM pipeline_conversations ORDER BY created_at ASC LIMIT 20",{})
|
||
system = AGENT_PROMPT.replace('__TOOLS__', TOOLS_TEXT).replace('__ENV__', env_text)
|
||
msgs = [{"role":"system","content":system}]
|
||
for h in (history or []):
|
||
msgs.append({"role":'user' if getattr(h,'role','')=='user' else 'assistant',"content":getattr(h,'content','')})
|
||
|
||
await sor.C('pipeline_conversations',{'id':getID(),'iteration_id':'','task_id':'','role':'user','content':message_text,'attachments':json.dumps(file_paths),'msg_type':'text','org_id':org_id,'created_by':uid})
|
||
|
||
# 处理上传文件:读取内容、发送文件widget、注入LLM上下文
|
||
file_contents = []
|
||
for fp in file_paths:
|
||
try:
|
||
if os.path.isfile(fp):
|
||
with open(fp, 'r', encoding='utf-8', errors='replace') as f:
|
||
content = f.read()[:8000]
|
||
fsize = os.path.getsize(fp)
|
||
fname = os.path.basename(fp)
|
||
file_contents.append(f"=== 文件: {fname} ===\n{content}")
|
||
# 发送文件widget
|
||
preview = content[:1000] + ("\n...(截断)" if len(content) > 1000 else "")
|
||
yield json.dumps({"type":"widget","widget":_widget_file(fname, preview, fsize)}, ensure_ascii=False) + '\n'
|
||
except Exception:
|
||
pass
|
||
|
||
# 把用户消息+文件内容合并为一条LLM消息
|
||
full_message = message_text
|
||
if file_contents:
|
||
full_message = message_text + "\n\n【附件文件内容】\n" + "\n\n".join(file_contents)
|
||
msgs.append({"role":"user","content":full_message})
|
||
|
||
agent_reply = ''
|
||
for turn in range(10):
|
||
try:
|
||
raw = await _call_llm(model, msgs, 0.4)
|
||
except Exception as e:
|
||
yield json.dumps({"type":"widget","widget":_widget_card("❌ LLM错误", _text(str(e)[:200]),"error")}, ensure_ascii=False) + '\n'
|
||
return
|
||
|
||
act = _parse(raw)
|
||
if act.get('action') == 'reply':
|
||
agent_reply = act.get('message', raw)
|
||
break
|
||
|
||
if act.get('action') == 'tool_call':
|
||
tool = act.get('tool','')
|
||
tparams = act.get('params',{})
|
||
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("失败")
|
||
|
||
# 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}"})
|
||
continue
|
||
|
||
agent_reply = raw
|
||
break
|
||
|
||
if not agent_reply:
|
||
agent_reply = "处理超时,请简化需求。"
|
||
|
||
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'})
|
||
|
||
# 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')
|
||
|
||
elif action == 'list_messages':
|
||
iteration_id = (params_kw or {}).get('iteration_id', '')
|
||
task_id = (params_kw or {}).get('task_id', '')
|
||
where = []
|
||
params = {}
|
||
if task_id: where.append("task_id=${t}$"); params["t"] = task_id
|
||
if iteration_id: where.append("iteration_id=${i}$"); params["i"] = iteration_id
|
||
if not where: where.append("1=1")
|
||
async with DBPools().sqlorContext(dbname) as sor:
|
||
ms = await sor.sqlExe(f"SELECT role,content,created_at FROM pipeline_conversations WHERE {' OR '.join(where)} ORDER BY created_at ASC LIMIT 100", params)
|
||
result = [{"role":getattr(m,'role',''),"content":getattr(m,'content',''),"created_at":str(getattr(m,'created_at',''))} for m in (ms or [])]
|
||
return json.dumps({"success":True,"messages":result}, ensure_ascii=False, default=str)
|
||
|
||
else:
|
||
return json.dumps({"error":f"Unknown action: {action}"}, ensure_ascii=False)
|