183 lines
9.1 KiB
Python
183 lines
9.1 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""PM 分析编排能力(方案乙,2026-09-04 用户选定)。
|
||
|
||
解析首跑任务不再由对账器并行硬派:对账器派「分析编排」任务给 PM,
|
||
PM 按任务链编排派发:评分项 →(资质 ∥ 要求+骨架)→ 成本收益。
|
||
分工:编排意图(顺序判断/异常招标文档的指令调整/交叉核对要点)归 PM;
|
||
依赖门禁(上游维度 QC 全过才准派下游)由代码强制,PM 跳步会被工具拒绝。
|
||
QC 退回重做(携带改进意见)仍由对账器自动派发,不归 PM。
|
||
"""
|
||
|
||
import json
|
||
import logging
|
||
|
||
from .bid_common import get_db, resolve_project_id, rec_to_dict, create_role_task, record
|
||
|
||
logger = logging.getLogger("pipeline.bidding")
|
||
|
||
MAX_PM_INSTRUCTIONS = 2000
|
||
|
||
|
||
async def _progress(sor, project_id):
|
||
"""各维度进度:产出计数 / 最新 QC / 在办任务 / 依赖门禁状态 / 裁剪状态。"""
|
||
from .bid_flow import (ANALYSIS_DIMS, DIM_QC_TYPES, DIM_UPSTREAM,
|
||
QC_OUTPUT_TABLE, _qc_passed_types, _open_dim_tasks)
|
||
from .bid_common import DIM_TO_STAGE
|
||
# 流程裁剪计划(None = 无确认计划 = 标准全流程)
|
||
try:
|
||
from pipeline_service.flow_plan_capability import plan_stage_enabled
|
||
stage_table = await plan_stage_enabled(project_id)
|
||
except Exception:
|
||
stage_table = None
|
||
passed = await _qc_passed_types(sor, project_id)
|
||
rows = []
|
||
for dim, label, qc_ts in ANALYSIS_DIMS:
|
||
info = {"dim": dim, "label": label}
|
||
if stage_table is not None and not stage_table.get(DIM_TO_STAGE.get(dim, ""), True):
|
||
info["裁剪"] = "已被用户裁剪(不得派发)"
|
||
rows.append(info)
|
||
continue
|
||
cnt = 0
|
||
for t in qc_ts:
|
||
tbl = QC_OUTPUT_TABLE[t]
|
||
recs = await sor.sqlExe(
|
||
"SELECT COUNT(*) AS c FROM " + tbl + " WHERE project_id=${p}$",
|
||
{"p": project_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
cnt += int(getattr(recs[0], "c", 0) if recs else 0)
|
||
info["产出条数"] = cnt
|
||
qc_brief = []
|
||
for t in qc_ts:
|
||
recs = await sor.sqlExe(
|
||
"SELECT round, fit_score, passed FROM bid_qc_reviews "
|
||
"WHERE project_id=${p}$ AND qc_type=${t}$ ORDER BY round DESC LIMIT 1",
|
||
{"p": project_id, "t": t})
|
||
await sor.sqlExe("COMMIT", {})
|
||
if recs:
|
||
r = rec_to_dict(recs[0])
|
||
qc_brief.append("%s 第%s轮 %s分 %s" % (
|
||
t, r.get("round"), r.get("fit_score"),
|
||
"通过" if str(r.get("passed")) == "1" else "未过"))
|
||
else:
|
||
qc_brief.append("%s 未审核" % t)
|
||
info["QC"] = ";".join(qc_brief)
|
||
info["在办任务"] = await _open_dim_tasks(sor, project_id, dim) > 0
|
||
up = DIM_UPSTREAM.get(dim, ())
|
||
miss = []
|
||
for u in up:
|
||
for t in DIM_QC_TYPES[u]:
|
||
if t not in passed:
|
||
miss.append("%s(%s)" % (u, t))
|
||
info["依赖门禁"] = "就绪" if not miss else "阻塞:%s 未通过" % "、".join(miss)
|
||
rows.append(info)
|
||
return rows
|
||
|
||
|
||
async def analysis_progress(project_id="", who=None, agent_id=None):
|
||
"""解析编排决策依据:各维度产出计数、最新 QC 结果、在办任务、依赖门禁状态(就绪/阻塞及缺什么)。"""
|
||
try:
|
||
project_id = await resolve_project_id(project_id)
|
||
except ValueError as e:
|
||
return "ERROR: %s" % str(e)[:200]
|
||
db, dbname = get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
rows = await _progress(sor, project_id)
|
||
return rows
|
||
|
||
|
||
async def dispatch_analysis_dim(dim, instructions="", project_id="",
|
||
who=None, agent_id=None):
|
||
"""派发一个分析维度的首跑任务(PM 编排专用)。
|
||
|
||
代码门禁:该维度的全部上游维度 QC 通过才准派发,否则拒绝(依赖是不变量)。
|
||
自动把已通过的上游维度(含 QC 轮次)注入任务参数 upstream_dims,
|
||
供分析员先读上游产出再抽取(交叉核对/矛盾照录)。
|
||
instructions:PM 对该维度的补充指令(异常招标文档的特殊交代/交叉核对要点)。
|
||
"""
|
||
from .bid_flow import (ANALYSIS_DIMS, DIM_QC_TYPES, DIM_UPSTREAM,
|
||
R_ANALYST, _qc_passed_types, _open_dim_tasks,
|
||
_tender_text_len, _create_split_tasks)
|
||
from .bid_common import DIM_TO_STAGE, get_thresholds
|
||
dims = {d: lb for d, lb, _ts in ANALYSIS_DIMS}
|
||
if dim not in dims:
|
||
return False, "未知维度 %s(可选:%s)" % (dim, "、".join(dims))
|
||
try:
|
||
project_id = await resolve_project_id(project_id)
|
||
except ValueError as e:
|
||
return False, str(e)[:200]
|
||
# 流程裁剪门禁(2026-09-11):被裁维度拒绝派发(用户已确认裁剪,PM 不得绕过)
|
||
try:
|
||
from pipeline_service.flow_plan_capability import plan_stage_enabled
|
||
_st = await plan_stage_enabled(project_id)
|
||
if _st is not None and not _st.get(DIM_TO_STAGE.get(dim, ""), True):
|
||
return False, ("维度「%s」已被用户在流程裁剪中确认裁掉,不得派发。"
|
||
"用 analysis_progress 查看当前裁剪后的维度清单。" % dims[dim])
|
||
except ImportError:
|
||
pass
|
||
label = dims[dim]
|
||
db, dbname = get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
if await _open_dim_tasks(sor, project_id, dim) > 0:
|
||
return False, "维度「%s」已有在办分析任务,不得重复派发" % label
|
||
passed = await _qc_passed_types(sor, project_id)
|
||
miss = []
|
||
for u in DIM_UPSTREAM.get(dim, ()):
|
||
for t in DIM_QC_TYPES[u]:
|
||
if t not in passed:
|
||
miss.append("%s(%s)" % (u, t))
|
||
if miss:
|
||
return False, ("依赖门禁:维度「%s」的上游未全部通过 QC——%s 未通过。"
|
||
"请先推进上游,不得跳步。" % (label, "、".join(miss)))
|
||
# 上游参照(已通过的维度 + QC 轮次),供分析员先读上游再抽取
|
||
up_refs = []
|
||
for u in DIM_UPSTREAM.get(dim, ()):
|
||
rnd = 0
|
||
for t in DIM_QC_TYPES[u]:
|
||
recs = await sor.sqlExe(
|
||
"SELECT round FROM bid_qc_reviews WHERE project_id=${p}$ "
|
||
"AND qc_type=${t}$ AND passed='1' ORDER BY round DESC LIMIT 1",
|
||
{"p": project_id, "t": t})
|
||
await sor.sqlExe("COMMIT", {})
|
||
if recs:
|
||
rnd = max(rnd, int(rec_to_dict(recs[0]).get("round") or 0))
|
||
up_refs.append({"dim": u, "label": dims[u], "qc_round": rnd})
|
||
pn = await sor.sqlExe(
|
||
"SELECT name FROM sd_projects WHERE id=${p}$", {"p": project_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
pname = rec_to_dict(pn[0]).get("name", "") if pn else project_id
|
||
params = {"stage": "analysis", "task_kind": "bid_analysis",
|
||
"analysis_dim": dim, "upstream_dims": up_refs,
|
||
"orchestrated_by": "agent.pm"}
|
||
if instructions:
|
||
params["pm_instructions"] = str(instructions)[:MAX_PM_INSTRUCTIONS]
|
||
# 大文件机械拆解(2026-09-15 用户要求:大任务必须拆小):正文超阈值时
|
||
# 直接派 N 个分段子任务(char_range 互不重叠、并行),不再派单体长任务。
|
||
_th = await get_thresholds(sor)
|
||
_tlen = await _tender_text_len(sor, project_id)
|
||
if _tlen > _th["split_chars"]:
|
||
_ids = await _create_split_tasks(
|
||
sor, project_id, dim, label, pname, params, _tlen,
|
||
_th["split_chars"], "run1")
|
||
await sor.sqlExe("COMMIT", {})
|
||
await record(sor, project_id, "pipeline_tasks", _ids[0] if _ids else "",
|
||
"pm_dispatch", who=who, agent_id=agent_id,
|
||
detail="PM 编排派发维度 %s 拆解 %d 段(正文 %d 字 > 阈值 %d)"
|
||
% (dim, len(_ids), _tlen, _th["split_chars"]))
|
||
await sor.sqlExe("COMMIT", {})
|
||
return True, ("已派发维度「%s」拆解分析:正文 %d 字超阈值 %d,拆为 %d 个分段"
|
||
"子任务并行(%s)" % (label, _tlen, _th["split_chars"],
|
||
len(_ids), "、".join(_ids)))
|
||
tid = await create_role_task(
|
||
sor, project_id, R_ANALYST,
|
||
"%s 招标文件分析:%s(PM编排,先读上游再抽取)" % (pname, label),
|
||
params)
|
||
await sor.sqlExe("COMMIT", {})
|
||
await record(sor, project_id, "pipeline_tasks", tid, "pm_dispatch",
|
||
who=who, agent_id=agent_id,
|
||
detail="PM 编排派发分析维度 %s(上游 %s)"
|
||
% (dim, ",".join(u["dim"] for u in up_refs) or "无"))
|
||
await sor.sqlExe("COMMIT", {})
|
||
return True, ("已派发维度「%s」分析任务 %s(上游参照 %d 个:%s)"
|
||
% (label, tid, len(up_refs),
|
||
"、".join(u["label"] for u in up_refs) or "无"))
|