yumoqing 4cb406003e 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 接口
2026-08-05 12:58:52 +08:00

191 lines
7.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""问题回路 — 角色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