pipeline-opportunity/pipeline_opportunity/opp_report_capability.py

213 lines
8.7 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.

"""商机产线报告/审批能力:研发报告编写 → 人工确认 → 研发审批流程。
状态机(REPORT_TRANSITIONS,opp_common):
draft → confirmed → approval_initiated → approved / rejected
└ rejected → draft(可重来)
门禁纪律:
- confirm 只能 draft → confirmed,且是人工决策(由人工任务回流驱动)
- initiate_approval 只能 confirmed → approval_initiated(未经确认禁止发起审批)
- 审批结论由人工决策回流(approve/reject),agent 不得自批自审
"""
import logging
from .opp_common import (
get_db, new_id, rec_to_dict, rows_to_dicts, can_transition,
RP_DRAFT, RP_CONFIRMED, RP_APPROVAL_INITIATED, RP_APPROVED, RP_REJECTED,
AP_INITIATED, AP_APPROVED, AP_REJECTED,
HT_REPORT_CONFIRM, HT_DEV_APPROVAL,
find_human_task, create_human_task, get_report,
)
logger = logging.getLogger("pipeline.opportunity.report")
async def create_report(project_id, software, title="", analysis="",
created_by="agent.opportunity"):
"""创建研发报告草稿。返回 (True, report_id) 或 (False, 错误)。"""
if not software:
return False, "缺少 software(软件/主题名)"
if not title:
title = "%s 研发机会分析报告" % software
db, dbname = get_db()
rid = new_id()
async with db.sqlorContext(dbname) as sor:
await sor.C("opp_reports", {
"id": rid,
"project_id": project_id or "",
"software": software,
"title": title,
"content": analysis or "",
"status": RP_DRAFT,
"created_by": created_by,
})
await sor.sqlExe("COMMIT", {})
return True, rid
async def update_report(report_id, content):
"""更新草稿报告内容(仅 draft 可改)。"""
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
rep = await get_report(sor, report_id)
if not rep:
return False, "报告不存在: %s" % report_id
if rep.get("status") != RP_DRAFT:
return False, "状态 %s 不可修改(仅草稿可编辑)" % rep.get("status")
await sor.U("opp_reports", {"id": report_id, "content": content})
await sor.sqlExe("COMMIT", {})
return True, "已更新"
async def list_reports(project_id="", status=""):
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
sql = ("SELECT id, project_id, software, title, status, created_by, "
"created_at, updated_at FROM opp_reports WHERE 1=1")
args = {}
if project_id:
sql += " AND project_id=${pid}$"
args["pid"] = project_id
if status:
sql += " AND status=${st}$"
args["st"] = status
sql += " ORDER BY created_at DESC LIMIT 50"
recs = await sor.sqlExe(sql, args)
await sor.sqlExe("COMMIT", {})
return rows_to_dicts(recs)
async def get_report_full(report_id):
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
rep = await get_report(sor, report_id)
return rep
async def submit_for_confirmation(report_id, note=""):
"""agent 完成报告后提交待人工确认:发人工确认任务(门禁)。
报告保持 draft,创建/复用 pending 人工任务。人工确认后才进 confirmed。
"""
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
rep = await get_report(sor, report_id)
if not rep:
return False, "报告不存在: %s" % report_id
if rep.get("status") != RP_DRAFT:
return False, "状态 %s 不可提交确认" % rep.get("status")
exist = await find_human_task(sor, rep.get("project_id", ""),
HT_REPORT_CONFIRM, status="pending")
if exist:
return True, "已有待确认任务: %s" % exist.get("id")
hid = await create_human_task(
sor, rep.get("project_id", ""), HT_REPORT_CONFIRM,
"确认研发报告:%s" % rep.get("title", ""),
note or "请审阅研发报告《%s》,确认后发起研发审批。" % rep.get("title", ""))
await sor.U("opp_reports", {
"id": report_id, "confirm_task_id": hid})
await sor.sqlExe("COMMIT", {})
return True, "已提交人工确认任务: %s" % hid
async def confirm_report(report_id, ok, operator=""):
"""人工确认回流(门禁)。ok=True → confirmed;ok=False → 退回草稿待改。"""
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
rep = await get_report(sor, report_id)
if not rep:
return False, "报告不存在: %s" % report_id
if rep.get("status") != RP_DRAFT:
return False, "状态 %s 无法确认(仅草稿可确认)" % rep.get("status")
new_status = RP_CONFIRMED if ok else RP_DRAFT
await sor.U("opp_reports", {
"id": report_id, "status": new_status,
"confirmed_by": operator or "human"})
# 完结对应的人工任务
hid = rep.get("confirm_task_id")
if hid:
await sor.U("pipeline_human_tasks", {
"id": hid, "status": "done" if ok else "rejected"})
await sor.sqlExe("COMMIT", {})
return True, "已确认" if ok else "已退回草稿"
async def initiate_approval(report_id, note="", created_by="agent.opportunity"):
"""发起研发审批:要求报告已 confirmed(人工确认门禁)。"""
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
rep = await get_report(sor, report_id)
if not rep:
return False, "报告不存在: %s" % report_id
if not await can_transition(rep.get("status"), RP_APPROVAL_INITIATED):
return False, "状态 %s 不可发起审批(须先人工确认)" % rep.get("status")
aid = new_id()
await sor.C("opp_approvals", {
"id": aid,
"report_id": report_id,
"project_id": rep.get("project_id", ""),
"status": AP_INITIATED,
"note": note,
"created_by": created_by,
})
await sor.U("opp_reports", {
"id": report_id, "status": RP_APPROVAL_INITIATED})
# 发人工审批任务
await create_human_task(
sor, rep.get("project_id", ""), HT_DEV_APPROVAL,
"研发审批:%s" % rep.get("title", ""),
note or "请审批《%s》的研发立项。" % rep.get("title", ""))
await sor.sqlExe("COMMIT", {})
return True, aid
async def resolve_approval(approval_id, result, operator=""):
"""审批结论回流(人工决策)。result=approved/rejected。"""
if result not in (AP_APPROVED, AP_REJECTED):
return False, "result 必须是 approved 或 rejected"
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
recs = await sor.sqlExe(
"SELECT id, report_id, status FROM opp_approvals WHERE id=${i}$",
{"i": approval_id})
await sor.sqlExe("COMMIT", {})
if not recs:
return False, "审批单不存在: %s" % approval_id
rec = rec_to_dict(recs[0])
if rec.get("status") != AP_INITIATED:
return False, "审批单状态 %s 无法结论" % rec.get("status")
report_status = RP_APPROVED if result == AP_APPROVED else RP_REJECTED
await sor.U("opp_approvals", {
"id": approval_id, "status": result,
"resolved_by": operator or "human"})
await sor.U("opp_reports", {
"id": rec.get("report_id"), "status": report_status})
# 完结人工审批任务
ht = await find_human_task(sor, rec.get("project_id", ""),
HT_DEV_APPROVAL, status="pending")
if ht:
await sor.U("pipeline_human_tasks", {
"id": ht.get("id"),
"status": "done" if result == AP_APPROVED else "rejected"})
await sor.sqlExe("COMMIT", {})
return True, result
async def list_approvals(project_id="", status=""):
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
sql = ("SELECT id, report_id, project_id, status, note, created_by, "
"created_at, resolved_at FROM opp_approvals WHERE 1=1")
args = {}
if project_id:
sql += " AND project_id=${pid}$"
args["pid"] = project_id
if status:
sql += " AND status=${st}$"
args["st"] = status
sql += " ORDER BY created_at DESC LIMIT 50"
recs = await sor.sqlExe(sql, args)
await sor.sqlExe("COMMIT", {})
return rows_to_dicts(recs)