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