- questions.py: agent_ask/answer_question 置 waiting/submitted 时清 claimed_by,否则 poll 器永不重新认领 - agent_loop_v2.py: _t_answer_question 回答后恢复任务 waiting→submitted(此前只标记问题已答,任务永久卡 waiting) - agent_loop.py: _claim_task 加 claimed_by IS NULL 原子认领 + updated_at 心跳;role/pm 循环心跳 COMMIT;pm 认领保持 review 态 - init.py: role poller 回收僵尸 running 任务、pm poller 回收僵尸 review 任务(心跳超时 10 分钟)
197 lines
7.6 KiB
Python
197 lines
7.6 KiB
Python
"""问题回路 — 角色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:
|
||
# 置 waiting 同时清 claimed_by,否则任务僵尸(poll器要求 claimed_by IS NULL 才会重新认领)
|
||
await sor.sqlExe(
|
||
"UPDATE pipeline_tasks SET state='waiting', claimed_by=NULL WHERE id=${tid}$",
|
||
{"tid": task_id})
|
||
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":
|
||
# 恢复 submitted 同时清 claimed_by,否则 poll 器永不重新认领该任务
|
||
await sor.sqlExe(
|
||
"UPDATE pipeline_tasks SET state='submitted', claimed_by=NULL WHERE id=${tid}$",
|
||
{"tid": task_id})
|
||
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
|