refactor: LLM-driven agent — list_projects/create_project/clone_repo/run_command tools, exact-match only
This commit is contained in:
parent
053b71ae95
commit
ae1aaadd91
@ -14,7 +14,14 @@ if action != 'send_message':
|
||||
action = 'send_message'
|
||||
dbname = get_module_dbname('pipeline-sdlc')
|
||||
|
||||
AGENT_PROMPT = """你是开发产线驾驶舱 Agent。需要数据时调工具,最终用 reply 回复。
|
||||
AGENT_PROMPT = """你是开发产线驾驶舱 Agent。用工具获取数据,用 reply 回复。
|
||||
|
||||
工作流:
|
||||
- 切换项目:先调 list_projects 看有哪些项目,再调 switch_project
|
||||
- 创建项目:如果需要的项目不存在,调 create_project
|
||||
- 提交任务前确保已切换到正确项目
|
||||
- clone_repo 用于克隆 git 仓库到工作空间
|
||||
- run_command 用于在工作空间中执行 shell 命令
|
||||
|
||||
输出格式二选一:
|
||||
{"action":"tool_call","tool":"工具名","params":{}}
|
||||
@ -24,13 +31,17 @@ AGENT_PROMPT = """你是开发产线驾驶舱 Agent。需要数据时调工具
|
||||
环境: __ENV__"""
|
||||
|
||||
TOOLS = [
|
||||
{"name":"switch_project","description":"切换项目","params":{"project":"名称"}},
|
||||
{"name":"list_projects","description":"列出所有可用项目","params":{}},
|
||||
{"name":"switch_project","description":"切换到指定项目(需先list_projects确认项目名)","params":{"project":"项目名称"}},
|
||||
{"name":"create_project","description":"创建新项目","params":{"name":"项目名称","description":"项目描述(可选)"}},
|
||||
{"name":"create_task","description":"提交任务","params":{"title":"标题","description":"描述","role":"requirement/design/develop/test/deploy"}},
|
||||
{"name":"list_tasks","description":"列出当前项目任务","params":{"state":"状态(可选)"}},
|
||||
{"name":"check_progress","description":"查进度","params":{"task_id":"任务ID(可选)"}},
|
||||
{"name":"list_tasks","description":"列出任务","params":{"state":"状态(可选)"}},
|
||||
{"name":"add_repo","description":"关联仓库","params":{"url":"git地址","name":"名称(可选)"}},
|
||||
{"name":"list_repos","description":"列出仓库","params":{}},
|
||||
{"name":"add_repo","description":"关联仓库到当前项目","params":{"url":"git地址","name":"名称(可选)"}},
|
||||
{"name":"list_repos","description":"列出当前项目仓库","params":{}},
|
||||
{"name":"clone_repo","description":"克隆仓库到工作空间","params":{"url":"git地址","branch":"分支名(可选,默认main)"}},
|
||||
{"name":"add_bug","description":"报告Bug","params":{"title":"标题","description":"描述"}},
|
||||
{"name":"run_command","description":"在工作空间中执行shell命令","params":{"cmd":"命令"}},
|
||||
]
|
||||
TOOLS_TEXT = json.dumps(TOOLS, ensure_ascii=False)
|
||||
|
||||
@ -47,23 +58,10 @@ def _guess_role(title):
|
||||
return 'develop'
|
||||
|
||||
async def _find_project(sor, name):
|
||||
"""Exact match only — LLM should use list_projects first to discover names."""
|
||||
if not name: return None
|
||||
# Exact match first
|
||||
recs = await sor.sqlExe("SELECT id,name FROM sd_projects WHERE name=${n}$",{"n":name})
|
||||
if recs: return recs[0]
|
||||
# Fuzzy match — require keyword >= 2 chars, prefer prefix match
|
||||
kw = name.lower().strip()
|
||||
if len(kw) < 2: return None
|
||||
allp = await sor.sqlExe("SELECT id,name FROM sd_projects ORDER BY created_at DESC",{})
|
||||
# First pass: name starts with kw
|
||||
for r in (allp or []):
|
||||
rn = (getattr(r,'name','') or '').lower()
|
||||
if rn.startswith(kw): return r
|
||||
# Second pass: kw is a word in name (split by common delimiters)
|
||||
for r in (allp or []):
|
||||
rn = (getattr(r,'name','') or '').lower()
|
||||
if kw in rn: return r
|
||||
return None
|
||||
return recs[0] if recs else None
|
||||
|
||||
async def _load_ctx(sor, uid):
|
||||
recs = await sor.sqlExe("SELECT current_project_id FROM pipeline_agent_settings WHERE user_id=${u}$",{"u":uid})
|
||||
@ -124,12 +122,33 @@ def _w_card(title, body, kind="success"):
|
||||
]}
|
||||
def _w_progress(text): return {"widgettype":"Text","options":{"text":text,"style":{"color":"#f59e0b","fontSize":"12px","padding":"2px 8px"}}}
|
||||
|
||||
_tool_labels = {'switch_project':'切换项目','create_task':'创建任务','check_progress':'检查进度','list_tasks':'查询任务','add_repo':'关联仓库','list_repos':'查看仓库','add_bug':'报告Bug'}
|
||||
_tool_labels = {'list_projects':'列出项目','switch_project':'切换项目','create_project':'创建项目','create_task':'创建任务','check_progress':'检查进度','list_tasks':'查询任务','add_repo':'关联仓库','list_repos':'查看仓库','clone_repo':'克隆仓库','add_bug':'报告Bug','run_command':'执行命令'}
|
||||
|
||||
async def _exec_tool(sor, tool, params, ctx, uid, org_id):
|
||||
p = params or {}
|
||||
try:
|
||||
if tool == 'switch_project':
|
||||
if tool == 'list_projects':
|
||||
allp = await sor.sqlExe("SELECT id,name,description,workspace_dir FROM sd_projects ORDER BY created_at DESC LIMIT 30",{})
|
||||
if not allp: return '无项目'
|
||||
lines = []
|
||||
for r in allp:
|
||||
marker = '★' if getattr(r,'id','') == ctx['pid'] else ' '
|
||||
lines.append(f"{marker} {getattr(r,'name','')} | {getattr(r,'description','') or ''} | ws:{getattr(r,'workspace_dir','') or ''}")
|
||||
return '\n'.join(lines)
|
||||
elif tool == 'create_project':
|
||||
name = (p.get('name','') or '').strip()
|
||||
if not name: return 'FAIL: 需要项目名称'
|
||||
ex = await sor.sqlExe("SELECT id FROM sd_projects WHERE name=${n}$",{"n":name})
|
||||
if ex: return f'FAIL: 项目「{name}」已存在'
|
||||
pid = getID()
|
||||
wid = getID()
|
||||
ws = f'/d/pipeline/workspaces/{org_id}/{name}'
|
||||
await sor.C('sd_projects',{'id':pid,'name':name,'description':p.get('description',''),'workspace_dir':ws,'org_id':org_id,'created_by':uid})
|
||||
await sor.C('sd_iterations',{'id':wid,'project_id':pid,'name':'默认迭代','org_id':org_id})
|
||||
await _save_ctx(sor, uid, pid)
|
||||
ctx['pid'] = pid; ctx['pname'] = name; ctx['ws'] = ws
|
||||
return f'OK: 已创建并切换到「{name}」'
|
||||
elif tool == 'switch_project':
|
||||
proj = await _find_project(sor, p.get('project',''))
|
||||
if proj:
|
||||
await _save_ctx(sor, uid, proj.id)
|
||||
@ -184,6 +203,39 @@ async def _exec_tool(sor, tool, params, ctx, uid, org_id):
|
||||
if not ctx['pid']: return 'FAIL: 请先切换到项目'
|
||||
rs = await sor.sqlExe("SELECT repo_name,repo_url FROM sd_project_repos WHERE project_id=${p}$",{"p":ctx['pid']})
|
||||
return '\n'.join([f"{r.repo_name}: {r.repo_url}" for r in rs]) if rs else '无仓库'
|
||||
elif tool == 'clone_repo':
|
||||
if not ctx['pid']: return 'FAIL: 请先切换到项目'
|
||||
url = p.get('url','')
|
||||
if not url: return 'FAIL: 需要仓库地址'
|
||||
branch = p.get('branch','main')
|
||||
ws = ctx.get('ws','')
|
||||
if not ws: return 'FAIL: 项目无工作空间路径'
|
||||
import subprocess, os
|
||||
repo_name = url.rstrip('/').split('/')[-1].replace('.git','')
|
||||
target = os.path.join(ws, repo_name)
|
||||
if os.path.exists(target): return f'FAIL: 目录已存在 {target}'
|
||||
os.makedirs(ws, exist_ok=True)
|
||||
cmd = f'git clone -b {branch} {url} {target}'
|
||||
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=120, cwd=ws)
|
||||
if r.returncode != 0: return f'FAIL: git clone失败: {r.stderr[:200]}'
|
||||
# Auto-add repo to project
|
||||
ex = await sor.sqlExe("SELECT id FROM sd_project_repos WHERE project_id=${p}$ AND repo_url=${u}$",{"p":ctx['pid'],"u":url})
|
||||
if not ex:
|
||||
await sor.C('sd_project_repos',{'id':getID(),'project_id':ctx['pid'],'repo_name':repo_name,'repo_url':url,'default_branch':branch,'local_path':target,'org_id':org_id})
|
||||
ctx['repos'].append({'n':repo_name,'u':url})
|
||||
return f'OK: 已克隆到 {target}'
|
||||
elif tool == 'run_command':
|
||||
import subprocess
|
||||
ws = ctx.get('ws','') or '/tmp'
|
||||
cmd = p.get('cmd','')
|
||||
if not cmd: return 'FAIL: 需要命令'
|
||||
# Safety: block destructive commands
|
||||
blocked = ['rm -rf /','mkfs.','dd if=',':(){','chmod 777 /']
|
||||
for b in blocked:
|
||||
if b in cmd: return 'FAIL: 危险命令被拦截'
|
||||
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60, cwd=ws)
|
||||
out = (r.stdout + r.stderr)[:2000]
|
||||
return f'OK (rc={r.returncode}):\n{out}' if out else f'OK (rc={r.returncode})'
|
||||
elif tool == 'add_bug':
|
||||
if not ctx['pid']: return 'FAIL: 请先切换到项目'
|
||||
title = p.get('title','')[:100]
|
||||
@ -230,7 +282,7 @@ if action == 'send_message':
|
||||
return
|
||||
|
||||
repos_str = ', '.join([r['n'] for r in ctx['repos']]) or '无'
|
||||
env_text = f"项目: {ctx['pname'] or '未选择'}\n仓库: {repos_str}"
|
||||
env_text = f"当前项目: {ctx['pname'] or '未选择'}\n工作空间: {ctx['ws'] or '未设置'}\n仓库: {repos_str}"
|
||||
system = AGENT_PROMPT.replace('__TOOLS__',TOOLS_TEXT).replace('__ENV__',env_text)
|
||||
msgs = [{"role":"system","content":system}]
|
||||
for h in (await sor.sqlExe("SELECT role,content FROM pipeline_conversations WHERE created_by=${u}$ ORDER BY created_at ASC LIMIT 20",{"u":uid}) or []):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user