- 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 接口
191 lines
7.2 KiB
Python
191 lines
7.2 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:
|
||
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
|