refactor: PM review to LLM tool-loop — can inspect code files before deciding
This commit is contained in:
parent
afa6972903
commit
851b2b2066
@ -256,6 +256,44 @@ __REPO_STATE__
|
||||
- test:测试覆盖充分、发现问题记录完整
|
||||
- deploy:部署配置完整、可一键部署"""
|
||||
|
||||
PM_SYSTEM_PROMPT = """你是项目经理(PM)。审核交付件并推动项目前进。
|
||||
|
||||
## 项目信息
|
||||
工作目录:__WORKSPACE__
|
||||
关联仓库:__REPOS__
|
||||
|
||||
## 待审核
|
||||
标题:__TITLE__
|
||||
角色:__ROLE__
|
||||
|
||||
## 仓库状态
|
||||
__REPO_STATE__
|
||||
|
||||
## 工具
|
||||
- read_file(path) — 读工作空间文件
|
||||
- list_files(path) — 列目录
|
||||
- git_status() — 查看git状态
|
||||
- run_shell(command) — 执行命令(编译/测试验证)
|
||||
|
||||
## 审核流程
|
||||
1. 先用工具检查代码是否实际产出(git_status/list_files/read_file)
|
||||
2. 如需编译验证,用 run_shell
|
||||
3. 最后给出决策
|
||||
|
||||
## 输出格式(每次一个JSON)
|
||||
查看文件:{"action":"tool_call","tool":"read_file","params":{"path":"相对路径"}}
|
||||
查看git:{"action":"tool_call","tool":"git_status","params":{}}
|
||||
批准:{"action":"review_approve","comment":"审核意见","next_task_title":"下阶段标题","next_task_description":"描述"}
|
||||
驳回:{"action":"review_reject","comment":"原因","questions":"修改要求"}
|
||||
完成:{"action":"review_complete","comment":"总结"}
|
||||
|
||||
审核标准:
|
||||
- requirement:需求是否清晰完整可量化
|
||||
- design:方案合理、覆盖需求、技术可行
|
||||
- develop:必须用 git_status/read_file 检查代码是否实际写入仓库
|
||||
- test:测试覆盖充分、发现问题记录完整
|
||||
- deploy:部署配置完整、可一键部署"""
|
||||
|
||||
|
||||
# ── Agent 核心逻辑 ──
|
||||
|
||||
@ -631,13 +669,11 @@ async def pm_review_run(project_id, agent_id=None, model_name=None):
|
||||
task_role = _normalize_role(getattr(task, "role", "") or "")
|
||||
workspace_dir = await _get_workspace_dir(sor, project_id)
|
||||
|
||||
# 提交 claim 事务,释放行锁,避免后续长操作(LLM调用)阻塞其他写入
|
||||
try:
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 首次审核时,确保仓库已 clone
|
||||
repos = await _get_project_repos(sor, project_id)
|
||||
if repos:
|
||||
await _setup_repos(sor, workspace_dir, project_id)
|
||||
@ -647,66 +683,79 @@ async def pm_review_run(project_id, agent_id=None, model_name=None):
|
||||
await sor.sqlExe("UPDATE pipeline_tasks SET state='submitted' WHERE id=${tid}$", {"tid": task_id})
|
||||
return {"status": "rejected", "task_id": task_id, "reason": "没有交付件"}
|
||||
|
||||
summary = deliverable_content[:200].replace('\n', ' ')
|
||||
content_preview = deliverable_content[:6000]
|
||||
if len(deliverable_content) > 6000:
|
||||
content_preview += "\n...(截断)"
|
||||
|
||||
repo_state = await _get_repo_state(workspace_dir)
|
||||
repos_str = ", ".join([r['name'] for r in repos]) if repos else "无"
|
||||
content_preview = deliverable_content[:6000]
|
||||
|
||||
prompt = (PM_PROMPT
|
||||
.replace('__TITLE__', title)
|
||||
.replace('__ROLE__', task_role)
|
||||
.replace('__SUMMARY__', summary)
|
||||
.replace('__DELIVERABLE__', content_preview)
|
||||
.replace('__WORKSPACE__', workspace_dir)
|
||||
.replace('__REPOS__', repos_str)
|
||||
.replace('__REPO_STATE__', repo_state))
|
||||
pm_system = PM_SYSTEM_PROMPT.replace('__TITLE__', title)\
|
||||
.replace('__ROLE__', task_role)\
|
||||
.replace('__WORKSPACE__', workspace_dir)\
|
||||
.replace('__REPOS__', repos_str)\
|
||||
.replace('__REPO_STATE__', repo_state)
|
||||
|
||||
from .llm_bridge import llm_call
|
||||
try:
|
||||
raw = await llm_call(prompt, model=model_name, temperature=0.3)
|
||||
except Exception as e:
|
||||
await sor.sqlExe("UPDATE pipeline_tasks SET state='failed' WHERE id=${tid}$", {"tid": task_id})
|
||||
return {"status": "failed", "task_id": task_id, "error": str(e)[:200]}
|
||||
msgs = [{"role": "system", "content": pm_system}]
|
||||
msgs.append({"role": "user", "content": f"请审核以下交付件(类型:{deliverable_type}):\n\n{content_preview}"})
|
||||
|
||||
parsed = _parse_result(raw)
|
||||
decision = parsed.get("status", "rejected")
|
||||
comment = parsed.get("comment", "")
|
||||
from .llm_bridge import llm_call_msgs
|
||||
|
||||
if decision == "approved":
|
||||
decision = None
|
||||
for turn in range(5):
|
||||
try:
|
||||
raw = await llm_call_msgs(msgs, model=model_name, temperature=0.3)
|
||||
except Exception as e:
|
||||
await sor.sqlExe("UPDATE pipeline_tasks SET state='failed' WHERE id=${tid}$", {"tid": task_id})
|
||||
return {"status": "failed", "task_id": task_id, "error": str(e)[:200]}
|
||||
|
||||
act = _parse_agent_action(raw)
|
||||
|
||||
if act.get('action') == 'review_approve':
|
||||
decision = {'status': 'approved', 'comment': act.get('comment', ''), 'next_title': act.get('next_task_title', ''), 'next_desc': act.get('next_task_description', '')}
|
||||
break
|
||||
elif act.get('action') == 'review_reject':
|
||||
decision = {'status': 'rejected', 'comment': act.get('comment', ''), 'questions': act.get('questions', '')}
|
||||
break
|
||||
elif act.get('action') == 'review_complete':
|
||||
decision = {'status': 'completed', 'comment': act.get('comment', '')}
|
||||
break
|
||||
elif act.get('action') == 'tool_call':
|
||||
tool = act.get('tool', '')
|
||||
params = act.get('params', {})
|
||||
result = await _exec_agent_tool(tool, params, workspace_dir)
|
||||
msgs.append({"role": "assistant", "content": raw})
|
||||
msgs.append({"role": "user", "content": f"工具 {tool} 结果:\n{result}"})
|
||||
else:
|
||||
decision = {'status': 'completed', 'comment': raw[:200]}
|
||||
break
|
||||
|
||||
if not decision:
|
||||
decision = {'status': 'rejected', 'comment': '审核超时'}
|
||||
|
||||
status = decision['status']
|
||||
comment = decision.get('comment', '')
|
||||
|
||||
if status == 'approved':
|
||||
from appPublic.uniqueID import getID
|
||||
pm_did = getID()
|
||||
await sor.C("pipeline_deliverables", {
|
||||
"id": pm_did, "project_id": project_id, "task_id": task_id,
|
||||
"deliverable_type": "pm_review",
|
||||
"title": f"PM审核:{title}",
|
||||
"content": json.dumps(parsed, ensure_ascii=False),
|
||||
"deliverable_type": "pm_review", "title": f"PM审核:{title}",
|
||||
"content": json.dumps(decision, ensure_ascii=False),
|
||||
"file_path": os.path.join(workspace_dir, 'deliverables', 'pm', f"{task_id}_review.md"),
|
||||
"quality_score": 100, "review_status": "approved",
|
||||
"created_by": agent_id or "pm",
|
||||
"quality_score": 100, "review_status": "approved", "created_by": agent_id or "pm",
|
||||
})
|
||||
await sor.sqlExe(
|
||||
"UPDATE pipeline_deliverables SET review_status='approved', review_comment=${cm}$ "
|
||||
"WHERE task_id=${tid}$", {"cm": comment, "tid": task_id})
|
||||
await sor.sqlExe(
|
||||
"UPDATE pipeline_tasks SET state=${st}$ WHERE id=${tid}$",
|
||||
{"st": TASK_APPROVED, "tid": task_id})
|
||||
|
||||
await sor.sqlExe("UPDATE pipeline_deliverables SET review_status='approved', review_comment=${cm}$ WHERE task_id=${tid}$", {"cm": comment, "tid": task_id})
|
||||
await sor.sqlExe("UPDATE pipeline_tasks SET state=${st}$ WHERE id=${tid}$", {"st": TASK_APPROVED, "tid": task_id})
|
||||
next_role = await _get_next_role(task_role)
|
||||
if next_role:
|
||||
next_tid, next_title = await _create_next_task(sor, project_id, task, next_role, comment)
|
||||
return {"status": "approved", "task_id": task_id,
|
||||
"next_task_id": next_tid, "next_role": next_role, "comment": comment}
|
||||
return {"status": "approved", "task_id": task_id, "next_task_id": next_tid, "next_role": next_role, "comment": comment}
|
||||
else:
|
||||
return {"status": "completed", "task_id": task_id, "comment": comment or "项目完成"}
|
||||
|
||||
elif decision == "rejected":
|
||||
elif status == 'rejected':
|
||||
from .questions import agent_ask
|
||||
rejection_q = parsed.get("questions") or comment or "交付件不满足要求"
|
||||
await agent_ask(project_id, task_id, "pm", rejection_q,
|
||||
context={"pm_comment": comment, "deliverable_type": deliverable_type})
|
||||
rejection_q = decision.get("questions") or comment or "交付件不满足要求"
|
||||
await agent_ask(project_id, task_id, "pm", rejection_q, context={"pm_comment": comment, "deliverable_type": deliverable_type})
|
||||
await sor.sqlExe("UPDATE pipeline_tasks SET state='submitted' WHERE id=${tid}$", {"tid": task_id})
|
||||
return {"status": "rejected", "task_id": task_id, "comment": comment, "question": rejection_q}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user