726 lines
41 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.

"""投标产线能力包(bidding_general):主 agent(驾驶舱)工具 + 角色定义 + 菜单 + slash 命令。
与开发产线(sdlc_general)同构,通过 pipeline-core 的 PipelineAbility 注册表挂载,
零侵入 core / AgentExecutor:项目 sd_projects.pipeline_id='bidding_general' 时自动生效。
角色任务流转不用 RoleSpec.next_role(全部留空),一律由 bid_flow 状态对账器驱动 —— 因为投标
产线是「一章一任务 + 两级回退循环」的扇出结构,不是线性任务链。
"""
import json
import logging
from pipeline_core import (
ToolDefinition, PipelineAbility, RoleSpec, FlowStage, FlowTemplate, register_ability,
SlashCommand, register_slash_command,
)
logger = logging.getLogger("pipeline.bidding.ability")
PIPELINE_ID = "bidding_general"
# ══════════════════ 预制流程阶段声明(流程裁剪机制,pipeline_flow_plans 消费)══════════════════
# trim: no=不可裁(流程不成立)/ warn=可裁但确认待办里给风险提示(质量门禁类)/ yes=自由裁
# deps 只写「硬依赖」(裁了下游不成立);QC 类门禁的放行语义在 bid_flow 里处理
#(qc_analysis 被裁 → 章节依赖自动视为通过),不进 deps。
# roles 用于:① 确认时收敛取消被裁阶段在办任务(仅当角色不属于任何保留阶段);
# ② 对账器按阶段门禁派发。
BID_FLOW_STAGES = [
FlowStage(key="analysis_scoring", label="评分项与得分规则解析",
description="从招标文件抽取评分项、得分规则、否决项——全流程交叉核对基准",
trim="no", roles=["agent.tender_analyst"]),
FlowStage(key="analysis_quals", label="资质清单解析",
description="抽取招标所需资质与要求(商务章与资料准备的上游)",
trim="warn", warn_note="裁掉资质解析后,商务章编写与资质文件准备失去依据,商务分可能失分",
roles=["agent.tender_analyst"], deps=["analysis_scoring"]),
FlowStage(key="analysis_reqs_outline", label="投标文件要求与章节骨架",
description="抽取投标文件格式/结构要求并生成章节骨架(评分项挂载)——所有章节编写的依据",
trim="no", roles=["agent.tender_analyst"], deps=["analysis_scoring"]),
FlowStage(key="analysis_cost_benefit", label="成本收益分析",
description="投标成本与收益测算(保证金/预算/付款条件),报价章的上游",
trim="yes", roles=["agent.tender_analyst"],
deps=["analysis_scoring", "analysis_reqs_outline"]),
FlowStage(key="qc_analysis", label="解析产出QC契合度审核",
description="QC 对照招标文件原文逐类审核解析产出(10分制,不过退回重做)",
trim="warn", warn_note="裁掉解析QC后,评分项/资质/骨架的抽取错误将不经核对直接进入编写阶段,可能整章返工",
roles=["agent.qc"], deps=["analysis_scoring", "analysis_reqs_outline"]),
FlowStage(key="prep", label="投标资料准备",
description="知识库资质匹配 + 缺失资质的人类文件清单(只阻塞商务章)",
trim="yes", roles=["agent.bid_prep"], deps=["analysis_quals"]),
FlowStage(key="chapter_write", label="分章节编写",
description="按章节骨架逐章编写(商务/技术分角色、并发派发)",
trim="no", roles=["agent.bid_writer", "agent.bid_biz_writer"],
deps=["analysis_reqs_outline"]),
FlowStage(key="chapter_review", label="章节评审",
description="每章对照评分项/投标文件要求评审打分,不达标退回重写(章节级质量循环)",
trim="warn", warn_note="裁掉章节评审后,章节写完即通过,无质量把关,整书评分(若保留)大概率反复打回",
roles=["agent.bid_reviewer"], deps=["chapter_write"]),
FlowStage(key="compose", label="标书合成",
description="全部章节通过后合成完整标书(md+docx)",
trim="no", roles=["agent.bid_compositor"], deps=["chapter_write"]),
FlowStage(key="whole_score", label="整书评分(模拟评标)",
description="按评分项+得分规则给整书打分,不达标改进意见落到章节退回重写",
trim="yes", roles=["agent.bid_scorer"], deps=["compose"]),
FlowStage(key="delivery_confirm", label="交付确认",
description="人工复核用印/密封/份数/递交后确认交付,项目完成",
trim="yes", roles=[], deps=["compose"]),
]
# ══════════════════ 纯技术方案流程(bid_tech_proposal,2026-09-14 批2)══════════════════
# 用户 2026-09-11 定义:输入=技术需求书(非招标文件),分析集中在技术层面六类评估项
# (功能清单/功能点/硬件配置/技术架构/功能架构/部署架构),方案书根据预定模版
# (无则 RAG/网络检索),模版骨架**由用户确认后**按章节分段编写和 QC 评审,
# 最终合成产出技术方案书。QC/评审**以用户提供的需求书为唯一基准**(对标此文件)。
TECH_PROPOSAL_STAGES = [
FlowStage(key="tech_analysis", label="技术需求解析(六类评估项)",
description="从技术需求书抽取六类评估项:功能清单/功能点/硬件配置/技术架构/功能架构/部署架构,落库 bid_tech_items",
trim="no", roles=["agent.tender_analyst"]),
FlowStage(key="tech_qc", label="技术评估项QC契合度审核",
description="QC 对照技术需求书原文逐类审核评估项(10分制,不过退回重做)",
trim="warn", warn_note="裁掉技术QC后,评估项抽取错误将不经核对直接进入骨架拟定与编写阶段,可能整章偏离需求",
roles=["agent.qc"], deps=["tech_analysis"]),
FlowStage(key="template_confirm", label="方案模版确定+用户确认骨架",
description="预定模版(知识库)→ 无则 RAG 检索 → 无则网络搜索,拟定章节骨架(覆盖六类评估项),发待办由用户确认后才开写",
trim="no", roles=["agent.pm"], deps=["tech_analysis"]),
FlowStage(key="tech_chapter_write", label="技术方案分章编写",
description="按用户确认的骨架逐章编写(一章一任务并发),内容对标需求书评估项写到操作层",
trim="no", roles=["agent.bid_writer"], deps=["template_confirm"]),
FlowStage(key="tech_chapter_qc", label="章节QC评审(对标需求书)",
description="每章对照技术需求书评估项覆盖度评审打分,不达标退回重写(章节级质量循环)",
trim="warn", warn_note="裁掉章节评审后,章节写完即通过,无需求覆盖度把关,方案书可能漏答需求条目",
roles=["agent.bid_reviewer"], deps=["tech_chapter_write"]),
FlowStage(key="tech_compose", label="合成技术方案书",
description="全部章节通过后合成完整技术方案书(md+docx)",
trim="no", roles=["agent.bid_compositor"], deps=["tech_chapter_write"]),
FlowStage(key="tech_delivery_confirm", label="交付确认",
description="人工核对技术方案书后确认交付,项目完成",
trim="yes", roles=[], deps=["tech_compose"]),
]
# 多流程模板声明(≥2 条时文件导入触发 LLM 自动判流 + 用户确认,2026-09-14 批2)
BID_FLOW_TEMPLATES = [
FlowTemplate(key="bid_standard", name="标准投标流程",
description="收到招标文件 → 四维度解析 → QC → 资料准备 → 分章编写 → 章节评审 → 合成标书 → 整书评分 → 交付",
detect_criteria=(
"输入文件是【招标文件/招标公告/投标邀请书】:含评标办法、评分标准、分值分配、"
"投标人资格要求、投标保证金、投标文件格式/密封/份数要求、开标时间等招投标程序性内容,"
"最终产出应是一份响应评分标准的【投标文件(标书)】。"),
stages=BID_FLOW_STAGES),
FlowTemplate(key="bid_tech_proposal", name="纯技术方案流程",
description="技术需求书 → 六类技术评估项解析 → QC → 模版骨架用户确认 → 分章编写 → 章节QC对标需求书 → 合成技术方案书",
detect_criteria=(
"输入文件是【技术需求书/需求规格说明书/产品规格书/技术方案要求】:主体内容是功能需求、"
"功能清单、功能点、性能指标、硬件配置、技术架构/功能架构/部署架构要求、接口/安全/验收标准等,"
"**没有**评标办法/评分标准/投标程序等招投标内容,最终产出应是一份【技术方案书】而非标书。"),
stages=TECH_PROPOSAL_STAGES),
]
# ══════════════════ 工具定义(主 agent / 驾驶舱)══════════════════
BID_TOOLS = [
# ── 招标文件(产线起点:收到招标书直接启动,无需立项/配人步骤)──
ToolDefinition(name="import_tender_file", description="导入招标文件并启动投标项目:用户提供了招标文件(附件/正文/文件路径)就调它。未建项目时自动立项(收到招标书直接启动),已有项目则导入到当前项目,系统自动开始解析",
parameters={"content_text": "招标文件正文(用户提供时必填)",
"file_path": "文件路径(与content_text二选一)",
"file_name": "文件名(可选)",
"project_name": "项目名称(可选,未建项目时自动立项用,缺省取文件名)"},
category="project"),
ToolDefinition(name="create_bid_project", description="创建空投标项目(仅当用户明确要先建项目、稍后再给招标文件时用)。若用户已提供招标文件,直接用 import_tender_file 自动立项",
parameters={"name": "项目名称", "description": "项目描述(可选)",
"tender_content": "招标文件正文(用户已提供时必填,直接导入跳过上传步骤)",
"tender_file_name": "招标文件名(可选)"}, category="project"),
ToolDefinition(name="request_tender_file", description="仅当项目确实没有任何招标文件且需要人工上传时才发「等待上传」人类待办(同一事项只发一次)。若文件已在工作空间/已登记,禁止调用——用 import_tender_file 登记",
parameters={"note": "说明(可选)"}, category="project"),
ToolDefinition(name="list_tender_files", description="查看已上传的招标文件及抽取状态", parameters={}, category="project"),
# ── 解析结果 ──
ToolDefinition(name="list_scoring_items", description="查看评分项与得分规则(从招标文件抽取的结果)",
parameters={"section": "评分部分(可选 business/technical/price)"}, category="analysis"),
ToolDefinition(name="list_doc_requirements", description="查看投标文件要求(章节结构/格式/密封/份数/截止)",
parameters={"req_type": "类型(可选 structure/format/seal/copies/deadline)"}, category="analysis"),
ToolDefinition(name="list_qualifications", description="查看招标所需资质及匹配状态(matched/missing/human_required)",
parameters={"match_status": "匹配状态(可选)"}, category="analysis"),
ToolDefinition(name="qc_report", description="查看解析产出 QC 契合度审核总览(评分项/资质/投标文件要求/章节骨架各类的最新得分与放行状态)",
parameters={}, category="analysis"),
# ── 知识库 ──
ToolDefinition(name="search_bid_kb", description="检索公司投标知识库(资质证书/业绩案例/模板/财务/人员)",
parameters={"query": "关键词", "doc_type": "类型(可选 qualification/cert/case/template/finance/personnel)"},
category="kb"),
ToolDefinition(name="list_kb_docs", description="列出公司投标知识库文档",
parameters={"doc_type": "类型(可选)"}, category="kb"),
ToolDefinition(name="add_kb_doc", description="向公司投标知识库录入文档(资质证书/业绩案例等)",
parameters={"doc_name": "文档名称", "doc_type": "类型", "doc_no": "编号(可选)",
"tags": "标签(可选)", "file_path": "文件路径(可选)", "customer": "案例客户(可选)",
"contract_amount": "合同金额(可选)"}, category="kb"),
ToolDefinition(name="request_human_docs", description="发布「人类应提供的文件」清单任务(知识库缺失的资质由人工提供)",
parameters={"items": "清单(可选,默认自动汇总缺失资质)", "note": "说明(可选)"}, category="kb"),
# ── 章节 / 标书 / 评分 ──
ToolDefinition(name="list_chapters", description="查看投标文件章节及状态(pending/writing/written/approved/rejected)",
parameters={"status": "状态(可选)"}, category="bid"),
ToolDefinition(name="chapter_detail", description="查看章节详情(要点/对应评分项/评审意见/正文)",
parameters={"chapter_id": "章节ID"}, category="bid"),
ToolDefinition(name="list_bid_documents", description="查看已合成的标书版本及评分", parameters={}, category="bid"),
ToolDefinition(name="list_reviews", description="查看评审记录(章节级/整书级)",
parameters={"scope": "范围(可选 chapter/whole)", "chapter_id": "章节ID(可选)"}, category="bid"),
ToolDefinition(name="bid_score_report", description="查看最近一轮整书评分报告(总分+逐项明细+改进意见)",
parameters={}, category="bid"),
# ── 材料缺口(占位符规范,2026-09-16)──
ToolDefinition(name="list_material_gaps", description="查看投标材料缺失清单(正文占位符自动登记的缺口:章节/材料/状态)",
parameters={"status": "状态(可选 pending/filled/waived)"}, category="bid"),
ToolDefinition(name="material_gap_report", description="生成材料缺失清单报告(markdown 全文,合成标书时也会自动落盘)",
parameters={}, category="bid"),
ToolDefinition(name="fill_material_gap", description="补齐材料缺口:用户提供了材料内容/知识库文档后调用,自动替换章节正文占位符(approved 章节保持通过不重审)",
parameters={"gap_id": "缺口ID", "fill_content": "材料正文(与kb_doc_id二选一)",
"kb_doc_id": "知识库文档ID(与fill_content二选一)"}, category="bid"),
ToolDefinition(name="waive_material_gap", description="放弃材料缺口(用户确认不提供该材料,正文占位符保留为如实披露)",
parameters={"gap_id": "缺口ID", "reason": "放弃理由(必填)"}, category="bid"),
# ── 诊断 / 推进 ──
ToolDefinition(name="diagnose_bid", description="诊断投标项目:卡在哪个阶段、门禁、章节分布、在办任务、最近评分",
parameters={}, category="agent"),
ToolDefinition(name="advance_bid", description="立即触发投标流程状态对账(补派应有的角色任务,通常无需手动调用)",
parameters={}, category="agent"),
ToolDefinition(name="reset_task_retry", description="人工处理完失败任务后恢复重试:把失败/挂起的任务清零重试计数并重新执行",
parameters={"task_id": "任务ID(从diagnose_bid看到的失败任务)"}, category="agent"),
]
# ══════════════════ 产线 prompt 片段 ══════════════════
BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从招标文件到标书交付的全流程管理:
收到招标文件 → 自动立项并解析(评分项/得分规则/资质/投标文件要求/章节骨架)
→ 解析产出 QC 契合度审核 → 资质文档准备 → 分章节编写 → 章节评审 → 合成标书 → 整书评分 → 交付确认。
产线没有独立的「立项/配人」步骤:用户把招标文件(附件/正文/文件路径)交给你,
调 import_tender_file 即自动建项目并启动解析——不要先问用户「要不要建项目/安排人员」。
招标信息的采集/摘要/审批归商机产线,本产线从「收到招标文件」起步;用户要采集招标信息时说明去商机产线。
## 流转由机制驱动(重要)
角色任务由系统状态对账器自动派发(每 15 秒一轮),你**不需要**手动为每个章节创建任务:
- 解析产出须经 QC 契合度审核:评分标准/得分规则/资质/投标文件要求/章节骨架逐类对照招标文件原文
按 10 分制打分,得分高于通过分(当前 8.5)才放行;不通过给改进意见 → 清空产出 → 解析重做 → 重审
- 章节评审不达标 → 章节自动退回重写
- 整书评分不达标 → 改进意见自动落到问题章节并退回重写 → 重新合成 → 重新评分
你的职责是:收到招标文件立即导入启动、处理卡点与提问、回答评审/评分中的人类决策。
只有诊断到「机制该派任务却没派」时,才用 advance_bid 手动触发一次对账,或用 diagnose_bid 查根因。
## 阻塞门禁(不要绕过)
以下人类任务 pending 时,系统不会给该项目派发任何 agent 任务:
- 等待上传招标文件(tender_file_upload,上传后自动完成)
- 等待人类提供资质文件(human_docs_request)
- 解析产出 QC 连续不达标(qc_escalation,人工修正产出或在审核记录页强制放行)
遇到项目"不动了",先 diagnose_bid 看是不是卡在门禁上,缺什么就催人补什么,不要硬推。
## 典型场景
- 用户交来招标文件(附件/正文/文件路径)→ 直接 import_tender_file(可带 project_name)。**未建项目时系统自动立项,无需先建项目或配人**——不要再让用户去界面手动上传。
- 用户明确说「先建个投标项目,文件稍后给」→ create_bid_project(name=XXX)。
- 项目已建、用户随后补交招标文件 → import_tender_file 导入(有当前项目就导入到该项目),跳过上传步骤。
- 用户问「招标文件上传了吗 / 解析出来了吗」→ list_tender_files 看上传与抽取状态。
- 用户问「解析质量怎么样 / QC 过了吗」→ qc_report,讲清哪类产出通过、哪类还在改。
- 用户问进展 → diagnose_bid(含章节分布、在办任务、最近评分),不要只报"正在进行"。
- 用户问「能得多少分 / 差在哪」→ bid_score_report + list_reviews,讲清失分项与改进意见落到了哪个章节。
- 用户要补资质/案例 → add_kb_doc 录入公司投标知识库;知识库缺的用 request_human_docs 让人工提供。
## 多流程模板(2026-09-15 新增,纯技术方案流程)
本产线有两条预制流程(show_flow_template 可列出):
- `bid_standard` 标准投标流程:输入=招标文件,产出=标书(默认);
- `bid_tech_proposal` 纯技术方案流程:输入=**技术需求书**(无评标办法/投标程序),产出=技术方案书——
六类技术评估项解析(功能清单/功能点/硬件配置/技术架构/功能架构/部署架构)→ QC 对标需求书
→ 模版骨架**用户确认**后才分章编写 → 合成技术方案书。
文件导入后系统会 LLM 自动判流并发「流程裁剪确认」待办(正文含识别结论与判据)——用户确认走哪条流程后才派发;
判流失败会发人工待办,此时**不要替用户选流程**,引导用户在会话里说明文件性质,用
propose_flow_plan(flow_key=...) 重新提案。用户明说「只要技术方案/这是需求书不是招标文件」时同样走 propose_flow_plan 指定。
## 流程裁剪(2026-09-11 新增)
用户提出「简化流程/跳过某阶段/只编写不评分」这类要求时:
1. 先 show_flow_template 列出预制流程各阶段(含可裁性);
2. 把用户要求映射为 trim_keys(要裁掉的阶段 key),调 propose_flow_plan 生成裁剪草案——
合法性由系统硬校验(不可裁阶段/依赖断裂会被拒绝并说明原因,你把原因如实转告用户);
3. 草案生成后系统自动发「流程裁剪确认」待办:**只有用户在待办里确认才生效,你没有确认权**。
告知用户去待办确认,并如实转述 warn 级阶段的风险提示;
4. 用户驳回 → get_flow_plan 读驳回意见 → 按意见修订 trim_keys 重新 propose(version 自动+1);
5. 裁剪只在项目启动阶段有效(已有任务产出结果后拒绝);确认后不可改。
裁剪被驳回期间对账器暂停派发——收到驳回后**主动**按意见修订重提,不要等用户催。
## 硬规则
- 招标金额、投标时间、资质要求这些要素,只能来自招标文件原文或人工确认,**禁止编造**;
抽取不到就如实说"文件未载明",必要时 ask_user。
- 标书内容的案例与业绩必须来自公司投标知识库的真实合同(bid_kb_docs 里 doc_type=case),
禁止虚构客户名、合同金额、历史成果数字。
- 章节没全部评审通过,不允许合成标书;评分没过门限,不允许当作可交付。
- **招标文件已在工作空间/已登记(list_tender_files 非空)时,禁止发上传待办**——
直接 import_tender_file(file_path=工作空间路径) 登记启动解析;系统对账器也会
自动登记工作空间里的招标书。同一事项的人类待办只发一次,禁止重复催。"""
# ══════════════════ 角色集 ══════════════════
BID_ROLES = [
RoleSpec(
name="agent.tender_analyst",
description="招标文件分析师",
aliases=["tender_analyst", "analyst", "analysis"],
system_prompt="你是招标文件分析师。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。",
next_role="",
task_title="招标文件解析",
),
RoleSpec(
name="agent.bid_prep",
description="投标资料专员",
aliases=["bid_prep", "prep", "qualification"],
system_prompt="你是投标资料专员。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。",
next_role="",
task_title="投标资料准备",
),
RoleSpec(
name="agent.bid_writer",
description="标书撰写工程师",
aliases=["bid_writer", "writer", "write"],
system_prompt="你是标书撰写工程师。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。",
next_role="",
task_title="章节编写",
),
RoleSpec(
name="agent.bid_reviewer",
description="标书评审员",
aliases=["bid_reviewer", "reviewer", "chapter_review"],
system_prompt="你是标书评审员。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。",
next_role="",
task_title="章节评审",
),
RoleSpec(
name="agent.bid_compositor",
description="标书合成工程师",
aliases=["bid_compositor", "compositor", "compose"],
system_prompt="你是标书合成工程师。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。",
next_role="",
task_title="标书合成",
),
RoleSpec(
name="agent.bid_scorer",
description="标书评分员(模拟评标)",
aliases=["bid_scorer", "scorer", "score"],
system_prompt="你是标书评分员(模拟评标专家)。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。",
next_role="",
task_title="整书评审与评分",
),
RoleSpec(
name="agent.pm",
description="投标项目经理(任务验收)",
aliases=["pm", "manager"],
system_prompt="你是投标项目经理。先 load_skill 加载 role 技能,按其中的职责与规范验收任务。",
next_role="",
),
RoleSpec(
name="agent.qc",
description="投标质量控制(合规检查)",
aliases=["qc", "quality"],
system_prompt="你是投标质量控制工程师。先 load_skill 加载 role 技能,按其中的职责与规范检查交付件。",
next_role="",
),
]
# ══════════════════ handler(签名 async def handler(sor, params, ctx) -> str)══════════════════
def _need_project(ctx):
pid = ctx.get("project_id", "") or ""
return pid, ("请先切换到投标项目(用 switch_project 或在项目列表选择)" if not pid else "")
def _fmt(ok, msg):
return ("OK: " if ok else "ERROR: ") + str(msg)
def _fmt_rows(rows, empty="(空)"):
if not rows:
return empty
if isinstance(rows, str):
return rows
return "\n".join(json.dumps(r, ensure_ascii=False, default=str) for r in rows[:80])
async def _h_create_bid_project(sor, p, ctx):
from .bid_tender_capability import create_bid_project
ok, msg = await create_bid_project(p.get("name", ""), description=p.get("description", ""),
tender_content=p.get("tender_content", ""),
tender_file_name=p.get("tender_file_name", ""),
org_id=ctx.get("org_id", "") or "",
created_by=ctx.get("user_id", "") or "",
who="agent.main_agent")
return _fmt(ok, msg)
async def _h_import_tender_file(sor, p, ctx):
# 无当前项目也允许:capability 内部自动立项(收到招标书直接启动)
pid = ctx.get("project_id", "") or ""
from .bid_tender_capability import import_tender_file
ok, msg = await import_tender_file(pid, content_text=p.get("content_text", ""),
file_path=p.get("file_path", ""),
file_name=p.get("file_name", ""),
project_name=p.get("project_name", ""),
org_id=ctx.get("org_id", "") or "",
created_by=ctx.get("user_id", "") or "",
who="agent.main_agent")
return _fmt(ok, msg)
async def _h_request_tender_file(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_tender_capability import request_tender_file
ok, msg = await request_tender_file(pid, note=p.get("note", ""), who="agent.main_agent")
return _fmt(ok, msg)
async def _h_list_tender_files(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_tender_capability import list_tender_files
return _fmt_rows(await list_tender_files(pid), "尚未上传招标文件")
async def _h_list_scoring_items(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_analysis_capability import list_scoring_items
return _fmt_rows(await list_scoring_items(pid, section=p.get("section", "") or ""),
"尚无评分项(等招标文件解析)")
async def _h_list_doc_requirements(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_analysis_capability import list_doc_requirements
return _fmt_rows(await list_doc_requirements(pid, req_type=p.get("req_type", "") or ""),
"尚无投标文件要求(等招标文件解析)")
async def _h_list_qualifications(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_kb_capability import list_qualifications
return _fmt_rows(await list_qualifications(pid, match_status=p.get("match_status", "") or ""),
"尚无资质要求(等招标文件解析)")
async def _h_search_bid_kb(sor, p, ctx):
from .bid_kb_capability import search_bid_kb
return _fmt_rows(await search_bid_kb(query=p.get("query", "") or "",
doc_type=p.get("doc_type", "") or "",
org_id=ctx.get("org_id", "") or ""),
"知识库无命中")
async def _h_list_kb_docs(sor, p, ctx):
from .bid_kb_capability import list_kb_docs
return _fmt_rows(await list_kb_docs(doc_type=p.get("doc_type", "") or "",
org_id=ctx.get("org_id", "") or ""),
"公司投标知识库为空(用 add_kb_doc 录入资质证书/业绩案例)")
async def _h_add_kb_doc(sor, p, ctx):
from .bid_kb_capability import add_kb_doc
ok, msg = await add_kb_doc(
p.get("doc_name", ""), doc_type=p.get("doc_type", "") or "qualification",
doc_no=p.get("doc_no", ""), tags=p.get("tags", ""), file_path=p.get("file_path", ""),
content_text=p.get("content_text", ""), issuer=p.get("issuer", ""),
customer=p.get("customer", ""), contract_amount=p.get("contract_amount", ""),
valid_until=p.get("valid_until", ""), org_id=ctx.get("org_id", "") or "0",
who="agent.main_agent")
return _fmt(ok, msg)
async def _h_request_human_docs(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_kb_capability import request_human_docs
ok, msg = await request_human_docs(pid, items=p.get("items", ""), note=p.get("note", ""),
who="agent.main_agent")
return _fmt(ok, msg)
async def _h_qc_report(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_qc_capability import qc_report
return await qc_report(pid)
async def _h_list_chapters(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_write_capability import list_chapters
return _fmt_rows(await list_chapters(pid, status=p.get("status", "") or ""),
"尚无章节(等招标文件解析生成章节骨架)")
async def _h_chapter_detail(sor, p, ctx):
from .bid_write_capability import chapter_detail
return await chapter_detail(p.get("chapter_id", ""))
async def _h_list_material_gaps(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_material_capability import list_material_gaps
return await list_material_gaps(pid, status=p.get("status", "") or "")
async def _h_material_gap_report(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_material_capability import material_gap_report
ok, rep = await material_gap_report(pid)
return rep if ok else "ERROR: %s" % rep
async def _h_fill_material_gap(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_material_capability import fill_material_gap
ok, msg = await fill_material_gap(
p.get("gap_id", ""), fill_content=p.get("fill_content", "") or "",
kb_doc_id=p.get("kb_doc_id", "") or "",
who=ctx.get("user_id", "") or "user", agent_id=ctx.get("agent_id", ""))
return msg if ok else "ERROR: %s" % msg
async def _h_waive_material_gap(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_material_capability import waive_material_gap
ok, msg = await waive_material_gap(
p.get("gap_id", ""), reason=p.get("reason", "") or "",
who=ctx.get("user_id", "") or "user", agent_id=ctx.get("agent_id", ""))
return msg if ok else "ERROR: %s" % msg
async def _h_list_bid_documents(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_compose_capability import list_bid_documents
return _fmt_rows(await list_bid_documents(pid), "尚未合成标书")
async def _h_list_reviews(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_review_capability import list_reviews
return _fmt_rows(await list_reviews(pid, scope=p.get("scope", "") or "",
chapter_id=p.get("chapter_id", "") or ""),
"尚无评审记录")
async def _h_bid_score_report(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_score_capability import bid_score_report
return await bid_score_report(pid)
async def _h_diagnose_bid(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_flow import diagnose_bid_project
return await diagnose_bid_project(pid)
async def _h_advance_bid(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_flow import reconcile_project
acts = await reconcile_project(sor, pid)
return "对账结果:\n" + "\n".join("- " + a for a in (acts or ["无动作"]))
async def _h_reset_task_retry(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
tid = str(p.get("task_id") or "").strip()
if not tid:
return "ERROR: 缺少 task_id"
from pipeline_service.task_capability import reset_task_retry
ok, msg = await reset_task_retry(tid, pid, who="agent.main_agent",
agent_id=ctx.get("agent_id", "") or "")
return _fmt(ok, "任务已恢复,对账器将重新派发" if ok else msg)
BID_HANDLERS = {
"create_bid_project": _h_create_bid_project,
"import_tender_file": _h_import_tender_file,
"request_tender_file": _h_request_tender_file,
"list_tender_files": _h_list_tender_files,
"list_scoring_items": _h_list_scoring_items,
"list_doc_requirements": _h_list_doc_requirements,
"list_qualifications": _h_list_qualifications,
"qc_report": _h_qc_report,
"search_bid_kb": _h_search_bid_kb,
"list_kb_docs": _h_list_kb_docs,
"add_kb_doc": _h_add_kb_doc,
"request_human_docs": _h_request_human_docs,
"list_chapters": _h_list_chapters,
"chapter_detail": _h_chapter_detail,
"list_material_gaps": _h_list_material_gaps,
"material_gap_report": _h_material_gap_report,
"fill_material_gap": _h_fill_material_gap,
"waive_material_gap": _h_waive_material_gap,
"list_bid_documents": _h_list_bid_documents,
"list_reviews": _h_list_reviews,
"bid_score_report": _h_bid_score_report,
"diagnose_bid": _h_diagnose_bid,
"advance_bid": _h_advance_bid,
"reset_task_retry": _h_reset_task_retry,
}
def _merge_shared():
"""并入引擎级通用任务/交付件/问答/项目工具(不重复实现,见 pipeline_service.shared_ability)。"""
try:
from pipeline_service.shared_ability import SHARED_TOOLS, SHARED_HANDLERS
except Exception as e: # pipeline_service 未安装/版本旧 → 只用投标专属工具,诚实降级
logger.warning("shared_ability 不可用,投标产线仅挂载专属工具: %s", str(e)[:120])
return list(BID_TOOLS), dict(BID_HANDLERS)
names = {t.name for t in BID_TOOLS}
tools = list(BID_TOOLS) + [t for t in SHARED_TOOLS if t.name not in names]
handlers = dict(SHARED_HANDLERS)
handlers.update(BID_HANDLERS) # 同名以投标产线实现为准
return tools, handlers
def register_bid_ability():
"""注册投标产线能力包(幂等)。"""
tools, handlers = _merge_shared()
ability = PipelineAbility(
pipeline_id=PIPELINE_ID,
name="通用投标产线",
tools=tools,
system_prompt=BID_PROMPT,
handlers=handlers,
roles=BID_ROLES,
flow_stages=BID_FLOW_STAGES,
flow_templates=BID_FLOW_TEMPLATES,
menus=[
{"label": "📄 招标文件", "icon": "", "url": "/pipeline-bidding/bid_tender_files/index.ui",
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
{"label": "📑 评分项", "icon": "", "url": "/pipeline-bidding/bid_scoring_items/index.ui",
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
{"label": "🔍 解析QC审核", "icon": "", "url": "/pipeline-bidding/bid_qc_reviews/index.ui",
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
{"label": "📝 标书章节", "icon": "", "url": "/pipeline-bidding/bid_chapters/index.ui",
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
{"label": "🎯 资质", "icon": "", "url": "/pipeline-bidding/bid_qualifications/index.ui",
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
{"label": "📚 投标知识库", "icon": "", "url": "/pipeline-bidding/bid_kb_docs/index.ui",
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
{"label": "📦 标书", "icon": "", "url": "/pipeline-bidding/bid_documents/index.ui",
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
{"label": "📋 任务树", "icon": "", "url": "/pipeline-bidding/bid_task_tree_popup.ui",
"type": "popup", "width": "92%", "height": "86%", "require_project": True},
],
)
register_ability(ability)
return ability
# ══════════════════ slash 命令(产线专属)══════════════════
async def _slash_files(args, ctx):
ex = ctx.get("executor")
if not ex:
return "无执行器上下文"
return await ex._execute_ability_tool("list_tender_files", {})
async def _slash_bid(args, ctx):
ex = ctx.get("executor")
if not ex:
return "无执行器上下文"
return await ex._execute_ability_tool("diagnose_bid", {})
async def _slash_chapters(args, ctx):
ex = ctx.get("executor")
if not ex:
return "无执行器上下文"
return await ex._execute_ability_tool("list_chapters", {"status": (args or "").strip()})
async def _slash_score(args, ctx):
ex = ctx.get("executor")
if not ex:
return "无执行器上下文"
return await ex._execute_ability_tool("bid_score_report", {})
async def _slash_task(args, ctx):
"""任务树 + 输入输出(ResourceBrowser 弹窗,与开发产线 /task 同构)。"""
base = (ctx.get("base_url") or "").rstrip("/")
tree_url = f"{base}/pipeline-bidding/api/bid_task_tree.dspy"
io_url = f"{base}/pipeline-bidding/api/bid_task_io.dspy"
return {
"widgettype": "PopupWindow",
"options": {
"title": "任务树 - 输入输出",
"width": "85%",
"height": "80%",
"auto_open": True,
},
"subwidgets": [{
"widgettype": "ResourceBrowser",
"id": "bid_task_rb",
"options": {
"tree_options": {
"widgettype": "Tree",
"options": {
"dataurl": tree_url,
"textField": "label",
"idField": "id",
"cfontsize": 1.0,
},
},
"browser_options": {
"widgettype": "urlwidget",
"options": {"url": io_url},
},
"tree_width": "42%",
},
}],
}
def register_bid_slash_commands():
for cmd in [
SlashCommand("files", "列出招标文件及解析状态", _slash_files, "pipeline", PIPELINE_ID),
SlashCommand("bid", "诊断当前投标项目卡点", _slash_bid, "pipeline", PIPELINE_ID),
SlashCommand("chapters", "列出标书章节及状态", _slash_chapters, "pipeline", PIPELINE_ID),
SlashCommand("score", "查看最近一轮整书评分报告", _slash_score, "pipeline", PIPELINE_ID),
SlashCommand("task", "任务树(任务状态+输入输出)", _slash_task, "pipeline", PIPELINE_ID),
]:
register_slash_command(cmd)
# import 即注册(与 sdlc_ability 一致:宿主 import pipeline_bidding 即生效)
register_bid_ability()
register_bid_slash_commands()