726 lines
41 KiB
Python
726 lines
41 KiB
Python
"""投标产线能力包(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()
|