551 lines
23 KiB
Python
551 lines
23 KiB
Python
"""Agent loop — 角色agent循环认领执行任务(v3.3.0)。
|
||
|
||
v3.3.0 新增:
|
||
- PM(项目经理)角色:审核交付件,决定通过/驳回
|
||
- 任务链:requirement → design → develop → test → deploy
|
||
前序角色完成后 → PM审核通过 → 自动创建下一角色任务
|
||
- 产物文件输出:交付件写入项目工作目录 deliverables/{role}/
|
||
- 工作目录自动创建:确保 workspace_dir 存在
|
||
|
||
架构:
|
||
- 主agent(cockpit_chat)理解客户意图,开发类意图写入 pipeline_tasks(带 role)。
|
||
- 各角色agent(需求/设计/开发/测试/运维)调 role_agent_run/role_agent_loop
|
||
循环认领自己角色的任务并执行。
|
||
- PM agent 调 pm_review_loop 循环审核处于 review 状态的任务。
|
||
- 认领是原子的:UPDATE ... WHERE state='submitted' + claimed_by 令牌校验,
|
||
多agent并发不会重复认领。
|
||
- 角色agent缺信息时返回 need_info → 写入 pipeline_agent_questions,
|
||
任务置 waiting,等主agent/客户回答后回到 submitted 重新认领继续。
|
||
- 只认领没有步骤记录的任务(带步骤的 DAG 任务归 executor 管)。
|
||
"""
|
||
|
||
import asyncio
|
||
import json
|
||
import os
|
||
import logging
|
||
|
||
logger = logging.getLogger("pipeline.agent_loop")
|
||
|
||
# 角色别名归一化:前端/主agent传的角色名统一到认领用的规范名
|
||
ROLE_ALIASES = {
|
||
'developer': 'develop', 'dev': 'develop', 'development': 'develop', 'coding': 'develop',
|
||
'requirement': 'requirement', 'requirements': 'requirement', 'requirement_analysis': 'requirement',
|
||
'designer': 'design', 'ui': 'design', 'ux': 'design',
|
||
'testing': 'test', 'qa': 'test', 'tester': 'test',
|
||
'deployment': 'deploy', 'release': 'deploy',
|
||
'operation': 'ops', 'operations': 'ops', 'maintenance': 'ops', '运维': 'ops',
|
||
'pm': 'pm', 'project_manager': 'pm', '项目经理': 'pm', 'manager': 'pm',
|
||
}
|
||
|
||
# 任务链:前序角色完成后,PM审核通过 → 自动创建下一角色任务
|
||
# None 表示链末端
|
||
ROLE_CHAIN = {
|
||
'requirement': 'design',
|
||
'design': 'develop',
|
||
'develop': 'test',
|
||
'test': 'deploy',
|
||
'deploy': None, # 部署完成 = 链结束
|
||
}
|
||
|
||
# PM 审核后任务状态
|
||
TASK_REVIEW = 'review' # 角色agent完成,待PM审核
|
||
TASK_APPROVED = 'approved' # PM审核通过
|
||
|
||
|
||
def _normalize_role(role):
|
||
r = (role or '').strip().lower()
|
||
return ROLE_ALIASES.get(r, r)
|
||
|
||
|
||
ROLE_PROMPT = """你是软件开发产线项目中的「__ROLE__」角色agent。
|
||
|
||
当前认领到的任务:
|
||
标题:__TITLE__
|
||
参数:__PARAMS__
|
||
__QNA__
|
||
|
||
## 项目信息
|
||
- 工作目录:__WORKSPACE__
|
||
- 你的产出物将保存到:__DELIVERABLE_DIR__
|
||
|
||
请站在你的角色立场完成这项工作。
|
||
|
||
## 产出规范
|
||
1. 产出内容必须完整、可交付、可直接使用
|
||
2. 产出格式:根据角色不同,产出对应格式:
|
||
- requirement(需求分析):需求规格文档(Markdown格式)
|
||
- design(设计):设计文档 / 架构图描述(Markdown格式)
|
||
- develop(开发):可运行的代码 + 代码说明
|
||
- test(测试):测试用例 + 测试报告(Markdown格式)
|
||
- deploy(部署):部署文档 / 配置 / 脚本
|
||
|
||
输出要求——返回纯 JSON(不要 markdown 包裹):
|
||
- 能完成时:{"status":"done","deliverable_type":"文档类型(requirement_doc/design_doc/code/test_report/deploy_doc)","result":"完整交付内容","summary":"一段话概述产出的关键结论"}
|
||
- 缺少关键信息无法继续时:{"status":"need_info","question":"要向主agent/客户提出的具体问题","partial":"已完成的部分"}"""
|
||
|
||
PM_PROMPT = """你是软件开发产线项目中的「项目经理(PM)」角色agent。
|
||
|
||
你的职责是审核其他角色agent产出的交付件,确保质量和完整性,并推动项目前进。
|
||
|
||
## 任务链
|
||
项目按照以下阶段推进:
|
||
1. requirement(需求分析)→ 2. design(设计)→ 3. develop(开发)→ 4. test(测试)→ 5. deploy(部署)
|
||
|
||
当前待审核的任务:
|
||
标题:__TITLE__
|
||
角色:__ROLE__
|
||
交付件内容:
|
||
__DELIVERABLE__
|
||
|
||
## 你的决策
|
||
请基于交付件内容做出判断,输出纯JSON(不要markdown包裹):
|
||
|
||
1. 审核通过,启动下一阶段:
|
||
{"status":"approved","comment":"审核意见","next_task_title":"下一阶段任务标题","next_task_description":"下一阶段任务详细描述"}
|
||
|
||
2. 审核不通过,需要修改:
|
||
{"status":"rejected","comment":"驳回原因和具体修改要求","questions":"需要角色agent澄清的具体问题"}
|
||
|
||
3. 项目已完成(deploy阶段通过后):
|
||
{"status":"completed","comment":"项目完成总结"}
|
||
|
||
## 审核标准
|
||
- requirement:需求是否清晰、完整、可量化
|
||
- design:方案是否合理、技术选型是否恰当、是否覆盖需求
|
||
- develop:代码是否可运行、是否遵循规范、是否实现设计
|
||
- test:测试覆盖是否充分、是否发现关键问题
|
||
- deploy:部署文档是否完整、配置是否正确"""
|
||
|
||
|
||
def _get_db():
|
||
from sqlor.dbpools import DBPools
|
||
db = DBPools()
|
||
if not db.databases:
|
||
from appPublic.jsonConfig import getConfig
|
||
config = getConfig()
|
||
if config.databases:
|
||
db.databases = config.databases
|
||
return db
|
||
|
||
|
||
def _resolve_workspace(workspace_dir):
|
||
"""解析工作目录,如果不可写则回退到 /tmp。确保目录存在。"""
|
||
if not workspace_dir:
|
||
workspace_dir = '/tmp/pipeline_workspaces/default'
|
||
# 检测是否可写
|
||
try:
|
||
parent = os.path.dirname(workspace_dir)
|
||
if parent and not os.access(parent, os.W_OK):
|
||
# 回退到 /tmp
|
||
proj_name = os.path.basename(workspace_dir)
|
||
workspace_dir = f'/tmp/pipeline_workspaces/{proj_name}'
|
||
except Exception:
|
||
workspace_dir = f'/tmp/pipeline_workspaces/{os.path.basename(workspace_dir) or "default"}'
|
||
# 确保目录存在
|
||
os.makedirs(workspace_dir, exist_ok=True)
|
||
return workspace_dir
|
||
|
||
|
||
async def _claim_task(sor, tenant_id: str, role: str, state: str = 'submitted'):
|
||
"""原子认领一条符合角色的任务。返回 task record 或 None。
|
||
|
||
认领策略:
|
||
1. 选出最早一条符合状态和角色的任务,
|
||
且排除有步骤记录的 DAG 任务(那些归 executor)。
|
||
2. UPDATE ... SET state='running', claimed_by=令牌 WHERE id=? AND state=原状态
|
||
3. 用令牌回查校验——别的agent抢先时校验失败,返回 None。
|
||
"""
|
||
role = _normalize_role(role)
|
||
recs = await sor.sqlExe(
|
||
"SELECT id, title, params, pipeline_id, tenant_id, role FROM pipeline_tasks "
|
||
"WHERE tenant_id=${tid}$ AND state=${state}$ AND (role=${role}$ OR role='') "
|
||
"AND NOT EXISTS (SELECT 1 FROM pipeline_task_steps s WHERE s.task_id=pipeline_tasks.id) "
|
||
"ORDER BY created_at ASC LIMIT 1",
|
||
{"tid": tenant_id, "state": state, "role": role})
|
||
if not recs:
|
||
return None
|
||
|
||
task = recs[0]
|
||
task_id = task.id
|
||
|
||
from appPublic.uniqueID import getID
|
||
claim_token = getID()
|
||
await sor.sqlExe(
|
||
"UPDATE pipeline_tasks SET state='running', claimed_by=${cb}$ "
|
||
"WHERE id=${tid}$ AND state=${state}$",
|
||
{"cb": claim_token, "tid": task_id, "state": state})
|
||
|
||
# 令牌校验:确认是本agent抢到的
|
||
check = await sor.sqlExe(
|
||
"SELECT id FROM pipeline_tasks WHERE id=${tid}$ AND state='running' AND claimed_by=${cb}$",
|
||
{"tid": task_id, "cb": claim_token})
|
||
if not check:
|
||
logger.info(f"claim lost race: task={task_id} role={role}")
|
||
return None
|
||
return task
|
||
|
||
|
||
async def _get_workspace_dir(sor, project_id):
|
||
"""获取项目的工作目录,确保可写并存在。"""
|
||
recs = await sor.sqlExe(
|
||
"SELECT workspace_dir FROM sd_projects WHERE id=${pid}$",
|
||
{"pid": project_id})
|
||
ws = getattr(recs[0], 'workspace_dir', '') if recs else ''
|
||
return _resolve_workspace(ws)
|
||
|
||
|
||
async def _build_qna_section(sor, task_id: str) -> str:
|
||
"""把该任务已回答的问答历史注入prompt,让角色agent带着答案继续。"""
|
||
from .questions import get_task_qna
|
||
qna = await get_task_qna(task_id)
|
||
if not qna:
|
||
return ""
|
||
lines = ["历史问答(这些问题已得到回答,请结合答案继续工作):"]
|
||
for item in qna:
|
||
src = item.get('answer_source') or ''
|
||
src_label = "客户" if src == "customer" else "主agent"
|
||
lines.append(f"问:{item.get('question', '')}")
|
||
lines.append(f"答({src_label}):{item.get('answer', '')}")
|
||
return "\n".join(lines)
|
||
|
||
|
||
def _parse_result(raw: str) -> dict:
|
||
"""解析角色agent的 LLM 返回。解析失败时按 done + 原文处理。"""
|
||
raw = (raw or "").strip()
|
||
if raw.startswith("```"):
|
||
raw = raw.split("\n", 1)[1].rsplit("```", 1)[0]
|
||
try:
|
||
d = json.loads(raw)
|
||
if isinstance(d, dict):
|
||
return d
|
||
except (json.JSONDecodeError, ValueError):
|
||
pass
|
||
return {"status": "done", "result": raw}
|
||
|
||
|
||
async def _write_deliverable_file(workspace_dir, role, task_id, deliverable_type, content):
|
||
"""将交付件写入项目工作目录的文件中。"""
|
||
role_dir = os.path.join(workspace_dir, 'deliverables', role)
|
||
os.makedirs(role_dir, exist_ok=True)
|
||
# 文件名:任务ID_类型.md
|
||
safe_type = (deliverable_type or 'deliverable').replace('/', '_')
|
||
filename = f"{task_id}_{safe_type}.md"
|
||
file_path = os.path.join(role_dir, filename)
|
||
try:
|
||
with open(file_path, 'w', encoding='utf-8') as f:
|
||
f.write(content or '')
|
||
logger.info(f"deliverable written: {file_path}")
|
||
return file_path
|
||
except Exception as e:
|
||
logger.error(f"failed to write deliverable file: {file_path} err={e}")
|
||
return ''
|
||
|
||
|
||
async def _get_deliverable_content(sor, task_id):
|
||
"""获取任务的最新交付件内容。"""
|
||
recs = await sor.sqlExe(
|
||
"SELECT content, deliverable_type FROM pipeline_deliverables "
|
||
"WHERE task_id=${tid}$ ORDER BY created_at DESC LIMIT 1",
|
||
{"tid": task_id})
|
||
if recs:
|
||
return getattr(recs[0], 'content', '') or '', getattr(recs[0], 'deliverable_type', '') or ''
|
||
return '', ''
|
||
|
||
|
||
async def _get_next_role(current_role):
|
||
"""获取任务链中的下一个角色。"""
|
||
return ROLE_CHAIN.get(_normalize_role(current_role))
|
||
|
||
|
||
async def _create_next_task(sor, project_id, task, next_role, pm_comment=''):
|
||
"""为下一个角色创建任务。"""
|
||
from appPublic.uniqueID import getID
|
||
title = getattr(task, 'title', '') or ''
|
||
params_str = getattr(task, 'params', '{}') or '{}'
|
||
try:
|
||
params = json.loads(params_str) if isinstance(params_str, str) else params_str
|
||
except (json.JSONDecodeError, TypeError):
|
||
params = {}
|
||
|
||
new_title = f"{title}({next_role}阶段)"
|
||
new_params = {
|
||
**params,
|
||
'previous_role': _normalize_role(getattr(task, 'role', '')),
|
||
'previous_task_id': getattr(task, 'id', ''),
|
||
'pm_comment': pm_comment,
|
||
}
|
||
|
||
new_task_id = getID()
|
||
await sor.C('pipeline_tasks', {
|
||
'id': new_task_id,
|
||
'tenant_id': project_id,
|
||
'pipeline_id': 'role_task',
|
||
'title': new_title,
|
||
'params': json.dumps(new_params, ensure_ascii=False),
|
||
'role': next_role,
|
||
'state': 'submitted',
|
||
'claimed_by': None,
|
||
})
|
||
logger.info(f"_create_next_task: {next_role} task={new_task_id} from {getattr(task, 'id', '')}")
|
||
return new_task_id, new_title
|
||
|
||
|
||
async def role_agent_run(project_id: str, role: str, agent_id: str = None, model_name: str = None) -> dict:
|
||
"""角色agent单次迭代:认领一条任务 → 执行 → 交付。
|
||
|
||
非PM角色完成 → 任务置 review(等PM审核)
|
||
PM角色 → 由 pm_review_run 处理
|
||
|
||
Returns:
|
||
{"status": "idle"} 没有待办任务
|
||
{"status": "completed", "task_id", "deliverable_id"} 完成交付(已进入review)
|
||
{"status": "need_info", "task_id", "question_id", "question"} 缺信息已提问
|
||
{"status": "failed", "task_id", "error"} 执行失败
|
||
"""
|
||
role = _normalize_role(role)
|
||
if role == 'pm':
|
||
# PM不执行角色任务,走pm_review_run
|
||
return {"status": "idle", "message": "PM agent请使用 pm_review_run"}
|
||
|
||
db = _get_db()
|
||
async with db.sqlorContext("pipeline") as sor:
|
||
task = await _claim_task(sor, project_id, role)
|
||
if not task:
|
||
return {"status": "idle", "message": "没有待办任务"}
|
||
|
||
task_id = task.id
|
||
title = getattr(task, "title", "") or ""
|
||
params_str = getattr(task, "params", "{}") or "{}"
|
||
|
||
# 获取工作目录
|
||
workspace_dir = await _get_workspace_dir(sor, project_id)
|
||
deliverable_dir = os.path.join(workspace_dir, 'deliverables', role)
|
||
os.makedirs(deliverable_dir, exist_ok=True)
|
||
|
||
qna_section = await _build_qna_section(sor, task_id)
|
||
prompt = (ROLE_PROMPT
|
||
.replace('__ROLE__', role)
|
||
.replace('__TITLE__', title)
|
||
.replace('__PARAMS__', params_str)
|
||
.replace('__QNA__', qna_section)
|
||
.replace('__WORKSPACE__', workspace_dir)
|
||
.replace('__DELIVERABLE_DIR__', deliverable_dir))
|
||
|
||
from .llm_bridge import llm_call
|
||
try:
|
||
raw = await llm_call(prompt, model=model_name, temperature=0.4)
|
||
except Exception as e:
|
||
await sor.sqlExe(
|
||
"UPDATE pipeline_tasks SET state='failed' WHERE id=${tid}$",
|
||
{"tid": task_id})
|
||
logger.error(f"role_agent_run llm failed: task={task_id} err={e}")
|
||
return {"status": "failed", "task_id": task_id, "error": str(e)[:200]}
|
||
|
||
parsed = _parse_result(raw)
|
||
|
||
# 缺信息 → 提问回路:任务置 waiting,等主agent/客户回答
|
||
if parsed.get("status") == "need_info" and parsed.get("question"):
|
||
from .questions import agent_ask
|
||
qid = await agent_ask(project_id, task_id, role, parsed["question"],
|
||
context={"partial": parsed.get("partial", ""), "title": title})
|
||
return {"status": "need_info", "task_id": task_id,
|
||
"question_id": qid, "question": parsed["question"]}
|
||
|
||
# 完成 → 写交付件到文件 + DB
|
||
from appPublic.uniqueID import getID
|
||
did = getID()
|
||
result_text = parsed.get("result") or raw
|
||
deliverable_type = parsed.get("deliverable_type") or role
|
||
summary = parsed.get("summary", "")
|
||
|
||
# 写文件
|
||
file_path = await _write_deliverable_file(
|
||
workspace_dir, role, task_id, deliverable_type, result_text)
|
||
|
||
# 写DB
|
||
await sor.C("pipeline_deliverables", {
|
||
"id": did, "project_id": project_id, "task_id": task_id,
|
||
"deliverable_type": deliverable_type,
|
||
"title": title, "content": result_text,
|
||
"file_path": file_path,
|
||
"quality_score": 80, "review_status": "pending",
|
||
"created_by": agent_id or role,
|
||
})
|
||
|
||
# 置为 review 状态,等待PM审核
|
||
await sor.sqlExe(
|
||
"UPDATE pipeline_tasks SET state=${st}$ WHERE id=${tid}$",
|
||
{"st": TASK_REVIEW, "tid": task_id})
|
||
logger.info(f"role_agent_run completed(review): task={task_id} role={role} deliverable={did} file={file_path}")
|
||
return {"status": "completed", "task_id": task_id, "deliverable_id": did,
|
||
"next_state": TASK_REVIEW, "file_path": file_path}
|
||
|
||
|
||
async def role_agent_loop(project_id: str, role: str, agent_id: str = None,
|
||
model_name: str = None, max_iterations: int = 10) -> list:
|
||
"""角色agent循环:连续认领执行,直到没有任务、需要提问或达到上限。"""
|
||
results = []
|
||
for _ in range(max_iterations):
|
||
r = await role_agent_run(project_id, role, agent_id, model_name)
|
||
results.append(r)
|
||
if r["status"] in ("idle", "need_info", "failed"):
|
||
break
|
||
await asyncio.sleep(1)
|
||
return results
|
||
|
||
|
||
# ── PM 审核 ──
|
||
|
||
async def pm_review_run(project_id: str, agent_id: str = None, model_name: str = None) -> dict:
|
||
"""PM审核单次迭代:认领一条 review 状态的任务 → 审核交付件 → 批准/驳回。
|
||
|
||
Returns:
|
||
{"status": "idle"} 没有待审核任务
|
||
{"status": "approved", "task_id", "next_task_id", "next_role"} 审核通过,已创建下一阶段任务
|
||
{"status": "rejected", "task_id"} 驳回,任务回到 submitted
|
||
{"status": "completed", "task_id"} 项目完成(链末端)
|
||
"""
|
||
db = _get_db()
|
||
async with db.sqlorContext("pipeline") as sor:
|
||
# 认领 review 状态的任务(不限角色,PM审核所有角色产出)
|
||
task = await _claim_task(sor, project_id, 'pm', state=TASK_REVIEW)
|
||
if not task:
|
||
return {"status": "idle", "message": "没有待审核任务"}
|
||
|
||
task_id = task.id
|
||
title = getattr(task, "title", "") or ""
|
||
task_role = _normalize_role(getattr(task, "role", "") or "")
|
||
|
||
# 获取交付件内容
|
||
deliverable_content, deliverable_type = await _get_deliverable_content(sor, task_id)
|
||
if not deliverable_content:
|
||
# 没有交付件 → 驳回
|
||
await sor.sqlExe(
|
||
"UPDATE pipeline_tasks SET state='submitted' WHERE id=${tid}$",
|
||
{"tid": task_id})
|
||
return {"status": "rejected", "task_id": task_id,
|
||
"reason": "没有找到交付件内容,任务已退回"}
|
||
|
||
# 截断过长内容(LLM有token上限)
|
||
content_preview = deliverable_content[:6000]
|
||
if len(deliverable_content) > 6000:
|
||
content_preview += "\n\n...(内容过长已截断)"
|
||
|
||
prompt = (PM_PROMPT
|
||
.replace('__TITLE__', title)
|
||
.replace('__ROLE__', task_role)
|
||
.replace('__DELIVERABLE__', content_preview))
|
||
|
||
from .llm_bridge import llm_call
|
||
try:
|
||
raw = await llm_call(prompt, model=model_name, temperature=0.3)
|
||
except Exception as e:
|
||
logger.error(f"pm_review_run llm failed: task={task_id} err={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]}
|
||
|
||
parsed = _parse_result(raw)
|
||
decision = parsed.get("status", "rejected")
|
||
comment = parsed.get("comment", "")
|
||
|
||
if decision == "approved":
|
||
# 获取工作目录
|
||
workspace_dir = await _get_workspace_dir(sor, project_id)
|
||
|
||
# 写PM审核记录到交付件
|
||
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),
|
||
"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",
|
||
})
|
||
|
||
# 更新原交付件审核状态
|
||
await sor.sqlExe(
|
||
"UPDATE pipeline_deliverables SET review_status='approved', "
|
||
"review_comment=${cm}$ WHERE task_id=${tid}$",
|
||
{"cm": comment, "tid": task_id})
|
||
|
||
# 任务置为 approved
|
||
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)
|
||
logger.info(f"pm_review: approved task={task_id}, created next={next_tid} role={next_role}")
|
||
return {"status": "approved", "task_id": task_id,
|
||
"next_task_id": next_tid, "next_role": next_role,
|
||
"next_title": next_title, "comment": comment}
|
||
else:
|
||
# 链末端(deploy审核通过 = 项目完成)
|
||
logger.info(f"pm_review: project completed, task={task_id} role={task_role}")
|
||
return {"status": "completed", "task_id": task_id,
|
||
"comment": comment or "项目所有阶段已完成"}
|
||
|
||
elif decision == "rejected":
|
||
# 驳回:任务回到 submitted,附带PM驳回原因作为question
|
||
from .questions import agent_ask
|
||
rejection_question = parsed.get("questions") or comment or "交付件不满足要求,请修改后重新提交"
|
||
await agent_ask(project_id, task_id, "pm", rejection_question,
|
||
context={"pm_comment": comment, "deliverable_type": deliverable_type})
|
||
# 任务回到 submitted(通过回答问题的resume机制)
|
||
# 但agent_ask已经把任务置waiting,需要让主agent/客户确认后才能回到submitted
|
||
# 简化为:直接回 submitted
|
||
await sor.sqlExe(
|
||
"UPDATE pipeline_tasks SET state='submitted' WHERE id=${tid}$",
|
||
{"tid": task_id})
|
||
logger.info(f"pm_review: rejected task={task_id}")
|
||
return {"status": "rejected", "task_id": task_id,
|
||
"comment": comment, "question": rejection_question}
|
||
|
||
else:
|
||
# completed(项目完成)
|
||
await sor.sqlExe(
|
||
"UPDATE pipeline_tasks SET state='completed' WHERE id=${tid}$",
|
||
{"tid": task_id})
|
||
return {"status": "completed", "task_id": task_id,
|
||
"comment": comment or "项目完成"}
|
||
|
||
|
||
async def pm_review_loop(project_id: str, agent_id: str = None,
|
||
model_name: str = None, max_iterations: int = 20) -> list:
|
||
"""PM审核循环:连续审核处于 review 状态的任务。"""
|
||
results = []
|
||
for _ in range(max_iterations):
|
||
r = await pm_review_run(project_id, agent_id, model_name)
|
||
results.append(r)
|
||
if r["status"] in ("idle", "failed"):
|
||
break
|
||
await asyncio.sleep(1)
|
||
return results
|
||
|
||
|
||
# ── 向后兼容别名(旧前端按 run_agent_loop/agent_loop 调用)──
|
||
|
||
async def run_agent_loop(project_id, role_name, model_name=None):
|
||
"""旧接口:单次迭代。role_name 为空时按 'dev' 处理。"""
|
||
role = _normalize_role(role_name or "develop")
|
||
if role == 'pm':
|
||
return await pm_review_run(project_id, model_name=model_name)
|
||
return await role_agent_run(project_id, role_name or "develop", model_name=model_name)
|
||
|
||
|
||
async def agent_loop(project_id, role_name, model_name=None, max_iterations=10):
|
||
"""旧接口:循环执行。role_name 为空时按 'dev' 处理。"""
|
||
role = _normalize_role(role_name or "develop")
|
||
if role == 'pm':
|
||
return await pm_review_loop(project_id, model_name=model_name, max_iterations=max_iterations)
|
||
return await role_agent_loop(project_id, role_name or "develop",
|
||
model_name=model_name, max_iterations=max_iterations)
|