ymq 4f2c6da6cf feat(team-communication): 通用问题冒泡引擎(多agent感知)
- communication.py: raise/resolve/escalate/list_for,问题类型→冒泡路径,处理方=role+agentid
- questions.py: 向后兼容层,委托 communication
- agent_loop.py: 角色提问(need_info)/PM退回(review_reject)/故障(fault_report)走 raise_problem;
  _build_qna_section 按 handler 路由注入;PM approved 只答结 review_reject
- sdlc_ability.py: list_questions/answer_question/escalate_question 按 handler 路由
- models: pipeline_agent_questions 加 from_agentid/problem_type/escalation_path/escalation_pos
2026-08-16 21:25:39 +08:00

98 lines
3.6 KiB
Python
Raw 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.

"""问题回路 — 向后兼容层,委托 communication.py 通用冒泡引擎。
历史状态 pending/forwarded/answered 收敛为通用模型:
- 问题类型(pipeline_problem_types)定义冒泡路径(escalation_path)
- 问题沿路径冒泡:解决→answered(停),没解决→pos+1(沿路径走),尽头=人兜底
- 归属不再靠 status 无差别路由,而靠 current_handler(escalation_path+pos)
本文件保留旧签名供 DSPY(env.question_*) 与既有调用方使用,内部全部走 communication.py。
"""
import json
import logging
from sqlor.dbpools import DBPools
from .communication import (
raise_problem,
resolve_problem,
forward_to_customer,
list_problems_for,
get_task_qa,
)
DBNAME = "pipeline"
logger = logging.getLogger("pipeline.questions")
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 / PM 提问。按 from_role 推断问题类型,走通用冒泡引擎。"""
# 审核退回(PM) vs 故障报告(cockpit/pm) vs 缺信息(角色):
ptype = "review_reject" if from_role == "pm" else (
"fault_report" if from_role == "cockpit" else "need_info")
return await raise_problem(
"", ptype, from_role, question,
task_id=task_id, tenant_id=tenant_id, context=context)
async def answer_question(question_id: str, answer: str, answered_by: str = "",
answer_source: str = "main_agent", resume: bool = True):
"""回答问题(解决 → 停止冒泡,任务恢复 submitted)。"""
return await resolve_problem(question_id, answer, answered_by, answer_source,
resume_task=resume)
async def forward_question(question_id: str):
"""主 agent 答不了 → 沿路径冒泡给客户(人)。"""
return await forward_to_customer(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:
"""客户视角:列出当前该「客户(人)」处理的问题。
旧签名兼容 status 过滤;无 status 时列客户待处理(pending)。
"""
rows = await list_problems_for("customer", tenant_id=tenant_id,
task_id=task_id, limit=limit)
if status:
rows = [r for r in rows if (r.get('status') or '') == status]
return rows
async def get_task_qna(task_id: str) -> list:
"""取任务的全部已回答问答(旧返回结构:list),供角色 agent 注入 prompt。"""
qa = await get_task_qa(task_id)
return qa.get('answered', []) if isinstance(qa, dict) else []