feat(v3.2.0): 主agent+角色agent架构 — role任务认领/问题回路
- pipeline_tasks 增加 role/claimed_by 字段+索引(存量库需ALTER) - agent_loop 重写为角色循环:原子认领(role匹配+令牌校验)→LLM执行→交付/need_info提问 - 角色别名归一化(developer→develop等),ROLE_PROMPT改replace防花括号崩溃 - questions.py 新增问题回路:agent_ask/answer_question/forward_question/list_questions - init.py 注册 pipeline_role_submit 与问题回路 env 接口
This commit is contained in:
parent
20896b6fa0
commit
4cb406003e
32
models/pipeline_agent_questions.json
Normal file
32
models/pipeline_agent_questions.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "pipeline_agent_questions",
|
||||
"title": "角色agent问题回路表",
|
||||
"primary": ["id"],
|
||||
"catelog": "entity"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "tenant_id", "title": "租户ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "task_id", "title": "任务ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "from_role", "title": "提问角色", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "question", "title": "问题内容", "type": "text"},
|
||||
{"name": "context", "title": "问题上下文(JSON)", "type": "text"},
|
||||
{"name": "answer", "title": "回答内容", "type": "text"},
|
||||
{"name": "answered_by", "title": "回答人", "type": "str", "length": 64, "nullable": "yes"},
|
||||
{"name": "answer_source", "title": "回答来源", "type": "str", "length": 16, "nullable": "yes"},
|
||||
{"name": "status", "title": "问题状态", "type": "str", "length": 32, "nullable": "no", "default": "pending"},
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
|
||||
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_paq_tenant", "idxtype": "index", "idxfields": ["tenant_id"]},
|
||||
{"name": "idx_paq_task", "idxtype": "index", "idxfields": ["task_id"]},
|
||||
{"name": "idx_paq_status", "idxtype": "index", "idxfields": ["status"]}
|
||||
],
|
||||
"codes": [
|
||||
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='question_state'"}
|
||||
]
|
||||
}
|
||||
@ -16,6 +16,8 @@
|
||||
{"name": "state", "title": "任务状态", "type": "str", "length": 32, "nullable": "no", "default": "submitted"},
|
||||
{"name": "current_version", "title": "当前版本号", "type": "int", "nullable": "no", "default": "1"},
|
||||
{"name": "params", "title": "提交参数", "type": "text"},
|
||||
{"name": "role", "title": "目标角色", "type": "str", "length": 32, "nullable": "no", "default": ""},
|
||||
{"name": "claimed_by", "title": "认领agent标识", "type": "str", "length": 64, "nullable": "yes"},
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
|
||||
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"}
|
||||
],
|
||||
@ -23,7 +25,8 @@
|
||||
{"name": "idx_pt_tenant", "idxtype": "index", "idxfields": ["tenant_id"]},
|
||||
{"name": "idx_pt_pipeline", "idxtype": "index", "idxfields": ["pipeline_id"]},
|
||||
{"name": "idx_pt_owner", "idxtype": "index", "idxfields": ["owner_id"]},
|
||||
{"name": "idx_pt_state", "idxtype": "index", "idxfields": ["state"]}
|
||||
{"name": "idx_pt_state", "idxtype": "index", "idxfields": ["state"]},
|
||||
{"name": "idx_pt_role", "idxtype": "index", "idxfields": ["role"]}
|
||||
],
|
||||
"codes": [
|
||||
{"field": "state", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='task_state'"}
|
||||
|
||||
@ -1,74 +1,216 @@
|
||||
"""Agent loop — fetches tasks, executes with LLM, delivers results."""
|
||||
"""Agent loop — 角色agent循环认领执行任务(v3.2.0)。
|
||||
|
||||
架构:
|
||||
- 主agent(cockpit_chat)理解客户意图,开发类意图写入 pipeline_tasks(带 role)。
|
||||
- 各角色agent(需求/设计/开发/测试/运维)调 role_agent_run/role_agent_loop
|
||||
循环认领自己角色的任务并执行。
|
||||
- 认领是原子的:UPDATE ... WHERE state='submitted' + claimed_by 令牌校验,
|
||||
多agent并发不会重复认领。
|
||||
- 角色agent缺信息时返回 need_info → 写入 pipeline_agent_questions,
|
||||
任务置 waiting,等主agent/客户回答后回到 submitted 重新认领继续。
|
||||
- 只认领没有步骤记录的任务(带步骤的 DAG 任务归 executor 管)。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
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',
|
||||
}
|
||||
|
||||
async def run_agent_loop(project_id, role_name, model_name=None):
|
||||
"""Single iteration: fetch pending task, execute, deliver."""
|
||||
|
||||
def _normalize_role(role):
|
||||
r = (role or '').strip().lower()
|
||||
return ROLE_ALIASES.get(r, r)
|
||||
|
||||
|
||||
ROLE_PROMPT = """你是软件开发产线项目中的「__ROLE__」角色agent。
|
||||
|
||||
当前认领到的任务:
|
||||
标题:__TITLE__
|
||||
参数:__PARAMS__
|
||||
__QNA__
|
||||
请站在你的角色立场完成这项工作。
|
||||
|
||||
输出要求——返回纯 JSON(不要 markdown 包裹):
|
||||
- 能完成时:{"status":"done","deliverable_type":"...","result":"完整交付内容"}
|
||||
- 缺少关键信息无法继续时:{"status":"need_info","question":"要向主agent/客户提出的具体问题","partial":"已完成的部分"}"""
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
async def _claim_task(sor, tenant_id: str, role: str):
|
||||
"""原子认领一条符合角色的 submitted 任务。返回 task record 或 None。
|
||||
|
||||
认领策略:
|
||||
1. 选出最早一条 state='submitted' 且 role 匹配(或 role 为空)的任务,
|
||||
且排除有步骤记录的 DAG 任务(那些归 executor)。
|
||||
2. UPDATE ... SET state='running', claimed_by=令牌 WHERE id=? AND state='submitted'
|
||||
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='submitted' 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, "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='submitted'",
|
||||
{"cb": claim_token, "tid": task_id})
|
||||
|
||||
# 令牌校验:确认是本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 _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 role_agent_run(project_id: str, role: str, agent_id: str = None, model_name: str = None) -> dict:
|
||||
"""角色agent单次迭代:认领一条任务 → 执行 → 交付或提问。
|
||||
|
||||
Returns:
|
||||
{"status": "idle"} 没有待办任务
|
||||
{"status": "completed", "task_id", "deliverable_id"} 完成交付
|
||||
{"status": "need_info", "task_id", "question_id", "question"} 缺信息已提问
|
||||
{"status": "failed", "task_id", "error"} 执行失败
|
||||
"""
|
||||
role = _normalize_role(role)
|
||||
db = _get_db()
|
||||
async with db.sqlorContext("pipeline") as sor:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT id, title, params, pipeline_id FROM pipeline_tasks "
|
||||
"WHERE state='submitted' AND tenant_id=${pid}$ LIMIT 1",
|
||||
{"pid": project_id},
|
||||
)
|
||||
if not recs:
|
||||
return {"status": "idle", "message": "No pending tasks"}
|
||||
task = await _claim_task(sor, project_id, role)
|
||||
if not task:
|
||||
return {"status": "idle", "message": "没有待办任务"}
|
||||
|
||||
task = recs[0]
|
||||
task_id = task.id
|
||||
await sor.sqlExe(
|
||||
"UPDATE pipeline_tasks SET state='running' WHERE id=${tid}$",
|
||||
{"tid": task_id},
|
||||
)
|
||||
title = getattr(task, "title", "") or ""
|
||||
params_str = getattr(task, "params", "{}") or "{}"
|
||||
|
||||
from pipeline_service.llm_bridge import llm_call
|
||||
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))
|
||||
|
||||
title = getattr(task, "title", "")
|
||||
params_str = getattr(task, "params", "{}")
|
||||
prompt = (
|
||||
f"Execute this development task:\n"
|
||||
f"Title: {title}\n"
|
||||
f"Params: {params_str}\n\n"
|
||||
f"Provide the result."
|
||||
)
|
||||
from .llm_bridge import llm_call
|
||||
try:
|
||||
result = await llm_call(prompt, model=model_name)
|
||||
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},
|
||||
)
|
||||
{"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]}
|
||||
|
||||
from appPublic.uniqueID import getID
|
||||
parsed = _parse_result(raw)
|
||||
|
||||
# 缺信息 → 提问回路:任务置 waiting,等主agent/客户回答
|
||||
if parsed.get("status") == "need_info" and parsed.get("question"):
|
||||
from pipeline_service.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"]}
|
||||
|
||||
# 完成 → 写交付件
|
||||
from appPublic.uniqueID import getID
|
||||
did = getID()
|
||||
result_text = parsed.get("result") or raw
|
||||
await sor.C("pipeline_deliverables", {
|
||||
"id": did, "project_id": project_id, "task_id": task_id,
|
||||
"deliverable_type": "code", "title": title, "content": result,
|
||||
"deliverable_type": parsed.get("deliverable_type") or role,
|
||||
"title": title, "content": result_text,
|
||||
"quality_score": 80, "review_status": "pending",
|
||||
"created_by": role_name,
|
||||
"created_by": agent_id or role,
|
||||
})
|
||||
await sor.sqlExe(
|
||||
"UPDATE pipeline_tasks SET state='completed' WHERE id=${tid}$",
|
||||
{"tid": task_id},
|
||||
)
|
||||
{"tid": task_id})
|
||||
logger.info(f"role_agent_run completed: task={task_id} role={role} deliverable={did}")
|
||||
return {"status": "completed", "task_id": task_id, "deliverable_id": did}
|
||||
|
||||
|
||||
async def agent_loop(project_id, role_name, model_name=None, max_iterations=10):
|
||||
"""Run agent loop until idle or max reached."""
|
||||
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 run_agent_loop(project_id, role_name, model_name)
|
||||
r = await role_agent_run(project_id, role, agent_id, model_name)
|
||||
results.append(r)
|
||||
if r["status"] == "idle":
|
||||
if r["status"] in ("idle", "need_info", "failed"):
|
||||
break
|
||||
await asyncio.sleep(2)
|
||||
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' 处理。"""
|
||||
return await role_agent_run(project_id, role_name or "dev", model_name=model_name)
|
||||
|
||||
|
||||
async def agent_loop(project_id, role_name, model_name=None, max_iterations=10):
|
||||
"""旧接口:循环执行。role_name 为空时按 'dev' 处理。"""
|
||||
return await role_agent_loop(project_id, role_name or "dev",
|
||||
model_name=model_name, max_iterations=max_iterations)
|
||||
|
||||
@ -30,9 +30,14 @@ from .step_registry import (
|
||||
unregister_step_type, load_builtin_types,
|
||||
)
|
||||
from .human import human_complete, approval_approve, approval_reject, human_list
|
||||
from .questions import (
|
||||
agent_ask, answer_question, forward_question,
|
||||
get_question, list_questions, get_task_qna,
|
||||
)
|
||||
from .agent_loop import role_agent_run, role_agent_loop, agent_loop, run_agent_loop
|
||||
|
||||
MODULE_NAME = "pipeline_service"
|
||||
MODULE_VERSION = "3.1.0"
|
||||
MODULE_VERSION = "3.2.0"
|
||||
|
||||
|
||||
async def pipeline_submit(tenant_id, pipeline_id, owner_id, title, params=None):
|
||||
@ -77,6 +82,40 @@ async def pipeline_submit(tenant_id, pipeline_id, owner_id, title, params=None):
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
|
||||
|
||||
async def pipeline_role_submit(tenant_id, pipeline_id, owner_id, title, params=None, role=""):
|
||||
"""提交角色任务(主agent → 角色agent 模式,v3.2.0)。
|
||||
|
||||
与 pipeline_submit 的区别:
|
||||
- 带 role 字段,由角色agent循环(role_agent_loop)认领执行
|
||||
- 不创建步骤记录、不启动 DAG executor
|
||||
|
||||
Args:
|
||||
role: 目标角色(requirement/design/dev/test/ops 等)。必填。
|
||||
|
||||
Returns:
|
||||
JSON string with success, task_id
|
||||
"""
|
||||
result = {"success": False}
|
||||
try:
|
||||
if not tenant_id:
|
||||
result["message"] = "缺少 tenant_id"
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
if not role:
|
||||
result["message"] = "缺少 role(角色任务必须指定目标角色)"
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
|
||||
params = params or {}
|
||||
task_id = await create_task(tenant_id, pipeline_id or "", owner_id, title, params, role=role)
|
||||
|
||||
result["success"] = True
|
||||
result["task_id"] = task_id
|
||||
result["role"] = role
|
||||
result["message"] = f"角色任务已提交(role={role}),等待角色agent认领执行"
|
||||
except Exception as e:
|
||||
result["message"] = str(e)
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
|
||||
|
||||
async def pipeline_list(tenant_id, pipeline_id=None, limit=100):
|
||||
"""查询租户的任务列表。"""
|
||||
result = {"success": False}
|
||||
@ -337,6 +376,7 @@ def load_pipeline_service():
|
||||
|
||||
# Task lifecycle
|
||||
env.pipeline_submit = pipeline_submit
|
||||
env.pipeline_role_submit = pipeline_role_submit
|
||||
env.pipeline_list = pipeline_list
|
||||
env.pipeline_detail = pipeline_detail
|
||||
env.pipeline_node = pipeline_node
|
||||
@ -370,11 +410,22 @@ def load_pipeline_service():
|
||||
# Register intent classifier + LLM bridge (shared across all pipelines)
|
||||
from .intent_classifier import intent_classify
|
||||
from .llm_bridge import llm_call
|
||||
from .agent_loop import agent_loop, run_agent_loop
|
||||
env.intent_classify = intent_classify
|
||||
env.pipeline_llm_call = llm_call
|
||||
env.agent_loop = agent_loop
|
||||
env.run_agent_loop = run_agent_loop
|
||||
|
||||
debug(f"[{MODULE_NAME}] v{MODULE_VERSION} loaded — pipeline engine with human-in-the-loop support")
|
||||
# Role-agent loop (v3.2.0: 主agent + 角色agent 认领执行)
|
||||
env.role_agent_run = role_agent_run
|
||||
env.role_agent_loop = role_agent_loop
|
||||
env.agent_loop = agent_loop # 向后兼容
|
||||
env.run_agent_loop = run_agent_loop # 向后兼容
|
||||
|
||||
# Question loop (角色agent ↔ 主agent ↔ 客户)
|
||||
env.agent_ask = agent_ask
|
||||
env.question_answer = answer_question
|
||||
env.question_forward = forward_question
|
||||
env.question_detail = get_question
|
||||
env.question_list = list_questions
|
||||
env.question_qna = get_task_qna
|
||||
|
||||
debug(f"[{MODULE_NAME}] v{MODULE_VERSION} loaded — pipeline engine with role-agent + question loop")
|
||||
return True
|
||||
|
||||
190
pipeline_service/questions.py
Normal file
190
pipeline_service/questions.py
Normal file
@ -0,0 +1,190 @@
|
||||
"""问题回路 — 角色agent缺信息时提问,主agent路由回答。
|
||||
|
||||
流程:
|
||||
1. 角色agent执行任务时发现缺关键信息 → agent_ask() 写入 pipeline_agent_questions
|
||||
(status=pending),任务置 waiting,退出本轮执行。
|
||||
2. 主agent轮询 pending 问题:
|
||||
- 能直接回答 → answer_question(answer_source='main_agent'),任务回 submitted,
|
||||
角色agent下轮重新认领并带着问答历史继续。
|
||||
- 答不了 → forward_question() 转客户 (status=forwarded);
|
||||
客户在会话中回答后,主agent调 answer_question(answer_source='customer')。
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from sqlor.dbpools import DBPools
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
DBNAME = "pipeline"
|
||||
logger = logging.getLogger("pipeline.questions")
|
||||
|
||||
# 问题状态
|
||||
Q_PENDING = "pending" # 待主agent回答
|
||||
Q_FORWARDED = "forwarded" # 主agent已转客户,等客户回复
|
||||
Q_ANSWERED = "answered" # 已回答
|
||||
|
||||
|
||||
def _get_db():
|
||||
db = DBPools()
|
||||
if not db.databases:
|
||||
from appPublic.jsonConfig import getConfig
|
||||
config = getConfig()
|
||||
if config.databases:
|
||||
db.databases = config.databases
|
||||
return db, DBNAME
|
||||
|
||||
|
||||
async def agent_ask(tenant_id: str, task_id: str, from_role: str, question: str, context: dict = None) -> str:
|
||||
"""角色agent提问。任务置 waiting,等待主agent/客户回答。返回 question_id。"""
|
||||
db, dbname = _get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
qid = getID()
|
||||
ctx_json = json.dumps(context, ensure_ascii=False, default=str) if context else None
|
||||
await sor.C('pipeline_agent_questions', {
|
||||
"id": qid,
|
||||
"tenant_id": tenant_id or "",
|
||||
"task_id": task_id or "",
|
||||
"from_role": from_role or "",
|
||||
"question": question or "",
|
||||
"context": ctx_json,
|
||||
"status": Q_PENDING,
|
||||
})
|
||||
if task_id:
|
||||
await sor.U('pipeline_tasks', {"id": task_id, "state": "waiting"})
|
||||
logger.info(f"agent_ask: task={task_id} role={from_role} qid={qid}")
|
||||
return qid
|
||||
|
||||
|
||||
async def answer_question(question_id: str, answer: str, answered_by: str = "",
|
||||
answer_source: str = "main_agent", resume: bool = True):
|
||||
"""回答问题。任务从 waiting 回 submitted,让角色agent重新认领继续执行。
|
||||
|
||||
Args:
|
||||
answer_source: 'main_agent' 或 'customer'
|
||||
resume: 是否把任务恢复为 submitted
|
||||
|
||||
Returns:
|
||||
更新后的问题记录 dict;问题不存在返回 None。
|
||||
"""
|
||||
db, dbname = _get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
recs = await sor.R('pipeline_agent_questions', {'id': question_id})
|
||||
if not recs:
|
||||
return None
|
||||
rec = recs[0]
|
||||
task_id = rec.task_id if hasattr(rec, 'task_id') else rec['task_id']
|
||||
|
||||
await sor.U('pipeline_agent_questions', {
|
||||
"id": question_id,
|
||||
"answer": answer or "",
|
||||
"answered_by": answered_by or "",
|
||||
"answer_source": answer_source or "main_agent",
|
||||
"status": Q_ANSWERED,
|
||||
})
|
||||
|
||||
resumed = False
|
||||
if resume and task_id:
|
||||
trecs = await sor.R('pipeline_tasks', {'id': task_id})
|
||||
if trecs:
|
||||
t = trecs[0]
|
||||
tstate = t.state if hasattr(t, 'state') else t['state']
|
||||
if tstate == "waiting":
|
||||
await sor.U('pipeline_tasks', {"id": task_id, "state": "submitted"})
|
||||
resumed = True
|
||||
|
||||
logger.info(f"answer_question: qid={question_id} task={task_id} source={answer_source} resumed={resumed}")
|
||||
return {
|
||||
"id": question_id,
|
||||
"task_id": task_id,
|
||||
"status": Q_ANSWERED,
|
||||
"answer_source": answer_source,
|
||||
"resumed": resumed,
|
||||
}
|
||||
|
||||
|
||||
async def forward_question(question_id: str):
|
||||
"""主agent答不了 → 转给客户。status: pending → forwarded。"""
|
||||
db, dbname = _get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
await sor.U('pipeline_agent_questions', {"id": question_id, "status": Q_FORWARDED})
|
||||
logger.info(f"forward_question: qid={question_id}")
|
||||
|
||||
|
||||
async def get_question(question_id: str):
|
||||
"""取单条问题记录(含 context 解析)。"""
|
||||
db, dbname = _get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
recs = await sor.R('pipeline_agent_questions', {'id': question_id})
|
||||
if not recs:
|
||||
return None
|
||||
rec = recs[0]
|
||||
if hasattr(rec, '__dict__'):
|
||||
d = {k: getattr(rec, k) for k in dir(rec) if not k.startswith('_')}
|
||||
else:
|
||||
d = dict(rec)
|
||||
ctx = d.get('context')
|
||||
if ctx and isinstance(ctx, str):
|
||||
try:
|
||||
d['context'] = json.loads(ctx)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
return d
|
||||
|
||||
|
||||
async def list_questions(tenant_id: str = None, task_id: str = None,
|
||||
status: str = None, limit: int = 50) -> list:
|
||||
"""问题列表,支持租户/任务/状态过滤。"""
|
||||
db, dbname = _get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
conditions = []
|
||||
params = {}
|
||||
if tenant_id:
|
||||
conditions.append("tenant_id=${tenant_id}$")
|
||||
params["tenant_id"] = tenant_id
|
||||
if task_id:
|
||||
conditions.append("task_id=${task_id}$")
|
||||
params["task_id"] = task_id
|
||||
if status:
|
||||
conditions.append("status=${status}$")
|
||||
params["status"] = status
|
||||
where = " AND ".join(conditions) if conditions else "1=1"
|
||||
try:
|
||||
limit = int(limit)
|
||||
except (TypeError, ValueError):
|
||||
limit = 50
|
||||
sql = f"SELECT * FROM pipeline_agent_questions WHERE {where} ORDER BY created_at DESC LIMIT {limit}"
|
||||
recs = await sor.sqlExe(sql, params)
|
||||
result = []
|
||||
for rec in (recs or []):
|
||||
if hasattr(rec, '__dict__'):
|
||||
d = {k: getattr(rec, k) for k in dir(rec)
|
||||
if not k.startswith('_') and not callable(getattr(rec, k))}
|
||||
else:
|
||||
d = dict(rec)
|
||||
ctx = d.get('context')
|
||||
if ctx and isinstance(ctx, str):
|
||||
try:
|
||||
d['context'] = json.loads(ctx)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
result.append(d)
|
||||
return result
|
||||
|
||||
|
||||
async def get_task_qna(task_id: str) -> list:
|
||||
"""取任务的全部已回答问答(按时间序),用于角色agent继续执行时注入prompt。"""
|
||||
db, dbname = _get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT question, answer, answer_source, from_role FROM pipeline_agent_questions "
|
||||
"WHERE task_id=${tid}$ AND status='answered' ORDER BY created_at ASC",
|
||||
{"tid": task_id})
|
||||
result = []
|
||||
for rec in (recs or []):
|
||||
result.append({
|
||||
"question": getattr(rec, 'question', '') or '',
|
||||
"answer": getattr(rec, 'answer', '') or '',
|
||||
"answer_source": getattr(rec, 'answer_source', '') or '',
|
||||
"from_role": getattr(rec, 'from_role', '') or '',
|
||||
})
|
||||
return result
|
||||
@ -47,8 +47,13 @@ async def get_pipeline_steps(pipeline_id: str) -> list:
|
||||
return result
|
||||
|
||||
|
||||
async def create_task(tenant_id: str, pipeline_id: str, owner_id: str, title: str, params: dict) -> str:
|
||||
"""Create a new pipeline task. Returns task_id."""
|
||||
async def create_task(tenant_id: str, pipeline_id: str, owner_id: str, title: str, params: dict, role: str = "") -> str:
|
||||
"""Create a new pipeline task. Returns task_id.
|
||||
|
||||
Args:
|
||||
role: 目标角色(需求/设计/开发/测试/运维)。空串=不限角色(兼容旧任务)。
|
||||
带角色的任务由角色agent循环认领执行,不带步骤记录。
|
||||
"""
|
||||
db, dbname = _get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
task_id = getID()
|
||||
@ -61,6 +66,7 @@ async def create_task(tenant_id: str, pipeline_id: str, owner_id: str, title: st
|
||||
"state": "submitted",
|
||||
"current_version": 1,
|
||||
"params": json.dumps(params, ensure_ascii=False, default=str),
|
||||
"role": role or "",
|
||||
}
|
||||
await sor.C('pipeline_tasks', data)
|
||||
return task_id
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user